В начале 2026 года мы перевели наше Android-приложение, насчитывающее более 170 экранов, с Navigation 2 на Navigation 3.
Navigation 3 предлагает типобезопасные точки назначения, сохраняемые стеки возврата и более современный API навигации. В целом миграция оказалась относительно простой, но по ходу работы мы всё же столкнулись с несколькими неожиданными сложностями — от обработки нижних панелей (bottom sheet) до исправления сбоев, которые проявились только после выпуска приложения.
В этой статье я расскажу, почему мы решили перейти на Navigation 3, как организовали процесс миграции, с какими проблемами столкнулись и какие выводы сделали.
Если вы тоже планируете переход на Navigation 3, наш опыт может сэкономить вам время и избавить от нескольких головных болей.
Жизнь с Navigation 2
Что было до миграции
Наше приложение было написано в 2021 году, а его навигация строилась на Jetpack Compose Navigation. В то время это было относительно новое и ещё не полностью стабилизированное решение от Google.
Поэтому, как и в любом уважающем себя крупном приложении, у нас были:
- устаревший код;
- функции-расширения для навигации;
- собственные уникальные решения для реализации возможностей, которых не хватало в стандартной библиотеке.
Важно также отметить, что нижние листы в нашем приложении тоже открывались и управлялись через систему навигации.
А затем появилась Navigation 3, предложившая принципиально новый, полностью переосмысленный подход к навигации.
Почему мы решили перейти на Navigation 3
Мы начали сопоставлять накопленный технический долг с будущими возможностями и в итоге решили провести миграцию. Для нас это было не просто обновлением библиотеки.
Мы рассматривали переход как инвестицию в платформу приложения.
Во-первых, в наших планах уже были версии для планшетов и складных устройств, а существующая реализация навигации плохо подходила для адаптивных компоновок и многооконных сценариев.
Во-вторых, навигационный слой постепенно обрастал функциями-расширениями и собственной логикой. Всё это работало, но поддержка каждой новой функции, связанной с навигацией, становилась всё дороже.
Мы понимали, что миграция связана с рисками, затрагивает значительную часть приложения и может привести к регрессиям. Однако откладывание перехода лишь увеличивало бы технический долг и делало будущую миграцию ещё более дорогой.
Что убедило нас окончательно
- Подход, изначально ориентированный на Compose. Весь пользовательский интерфейс и вся навигация приложения уже были построены на Compose.
- MVI и навигация, управляемая состоянием. Navigation 3 рассматривает навигационный стек как обычное состояние. Это очень естественно сочеталось с нашей архитектурой MVI.
Концептуально навигация стала выглядеть примерно так:
val navigationState = rememberNavigationState(
startKey = MainScreenPointScreen,
topLevelKeys = setOf(MainScreenPointScreen)
)
val navigator = remember { Navigator(navigationState) }
fun navigate(key: NavKey) {
when (key) {
state.currentTopLevelKey -> clearSubStack()
in state.topLevelKeys -> goToTopLevel(key)
else -> goToKey(key)
}
}
fun goBack(): Boolean {
backStack.removeLastOrNull()
return true
}
Навигация стала гораздо ближе к подходу, основанному на состоянии, который мы уже применяли в других частях приложения.
- Адаптивные компоновки. В бизнес-плане была предусмотрена поддержка планшетов и Pixel Fold.
- Требования Google. Google активно продвигает адаптивные компоновки и поддержку устройств с большими экранами. Более того, начиная с Android 17, принудительная фиксация ориентации экрана фактически становится устаревшим подходом для большинства приложений. Google Play также будет официально отдавать приоритет приложениям, поддерживающим большие экраны.
Поэтому для нас выбор выглядел так: либо выполнить миграцию сейчас, в контролируемых условиях, либо всё равно вернуться к ней позже, но уже под значительно большим давлением.
Разработчикам, которые только начинают создавать новое приложение, я бы также рекомендовал изначально проектировать архитектуру с использованием Navigation 3.
Как проходила миграция и есть ли жизнь после неё
Ноябрь 2025 года
Я увидел, что вышла стабильная версия Navigation 3, и как инициативный разработчик, вдохновлённый новым подходом к навигации, создал задачу на исследование для следующего спринта.
Цель исследования была простой: оценить возможность миграции, масштаб затрагиваемого кода и риски, а также понять, насколько такой переход вообще реалистичен для приложения нашего размера. Довольно быстро стало ясно: легко не будет.
Я сразу определил главный риск — выполнить миграцию поэтапно невозможно. Из-за отсутствия обратной совместимости между Navigation 2 и Navigation 3 переход возможен только при одновременной миграции всех экранов. Частичный переход не предусмотрен.
После исследования мы вместе с руководителями команд оценили риски, возможные регрессии и план развития продукта на ближайшие кварталы. Поскольку внедрение адаптивных макетов уже было запланировано, миграция получила одобрение.
Февраль 2026 года
Во время планирования следующего спринта мне выделили отдельный спринт на миграцию. На протяжении всех двух долгих и интересных недель, наполненных болью, я занимался исключительно навигацией. Вся миграция выполнялась вручную.
Да, я пробовал использовать искусственный интеллект. Однако на тот момент задача оказалась слишком сложной для кодовой базы с большим количеством устаревшего кода. Из-за функций-расширений, собственных навигационных решений и нетривиальных зависимостей Claude Code периодически выдавал противоречивый результат, плохо подходящий для нашей конкретной системы. Вместо миграции на Navigation 3 он возвращал код для Navigation 2 и писал: «Миграция успешно завершена, теперь проект работает на последней стабильной версии навигации». Но к этому моменту мы ещё вернёмся.
Переход на Navigation 3 занял у меня две долгие недели с ежедневными переработками. Небольшое уточнение: нет, компания меня не заставляла. Да, мне дали бы столько времени, сколько потребовалось бы. И да, никто не заставлял меня писать эту статью.
Забавный факт: Gemini и Claude Code оценили такую миграцию в 6–8 недель работы целой команды. В итоге я выполнил её один за две недели.
Переработки. Почему?
У масштабного переустройства кода есть один серьёзный недостаток — конфликты при слиянии изменений. Мы сознательно решили не останавливать разработку на время миграции, поскольку для бизнеса это было бы неоправданно дорого. Команда продолжала создавать новые возможности, пока я параллельно занимался переходом на новую навигацию.
И тут начинается самое интересное: пока я вручную переписываю каждый экран приложения, мои коллеги продолжают писать код, создавать новые экраны и добавлять новые возможности — всё ещё на старой навигации. Да, я пытался договориться, чтобы они на это время отдохнули, но ничего не получилось.
Поэтому я быстро понял: миграцию нужно закончить как можно скорее. Чем дольше она продолжается, тем дороже и болезненнее становится разрешение конфликтов при слиянии изменений.
Неожиданные сложности
Bottom Sheets
При планировании миграции я упустил одну важную деталь — нижние панели. В Navigation 3 не было готового штатного решения для их отображения через систему навигации.
Мне пришлось написать собственную, не слишком изящную обёртку вокруг entry<>, передавать через метаданные признак того, что экран должен отображаться как нижняя панель, а затем, если этот признак установлен, показывать экран через ModalBottomSheetLayout из Material 3 вместо NavDisplay.
val NavEntry<out NavKey>.isBottomSheet: Boolean
get() = metadata["isBottomSheet"] as? Boolean == true
inline fun <reified T : NavKey> EntryProviderScope<NavKey>.bottomSheetEntry(
noinline content: @Composable (T) -> Unit,
) {
entry<T>(
metadata = mapOf("isBottomSheet" to true),
content = content
)
}
NavDisplay(
entries = navigationState.toEntries(mainEntryProvider)
.filter { it.isBottomSheet.not() },
onBack = { navigator.goBack() },
)
Это решение было далеко не идеальным — скорее временным обходным вариантом, который позволил нам продолжить миграцию. Сейчас для этого есть более удачные подходы на основе SceneStrategy, и позднее мы заменили первоначальную реализацию.
Глубокие ссылки
Неожиданным преимуществом миграции стала возможность полностью пересмотреть работу с глубокими ссылками.
Исторически глубокие ссылки в нашем приложении были организованы не лучшим образом, и не всегда было очевидно, какой именно экран обрабатывает конкретную ссылку.
Во время миграции я собрал всю связанную с ними логику в одном месте и централизовал её. Это дало простой, но весьма ощутимый результат: теперь стало значительно легче понять, что именно происходит при обработке каждой глубокой ссылки.
До миграции:
composable(
deepLinks = "$uri/${ScreenName.PROFILE.value}/?$PROFILE_ID={$PROFILE_ID}" +
"&$EVENT_CONTEXT={$EVENT_CONTEXT}" +
"&$EVENT_ANCHOR={$EVENT_ANCHOR}" +
"&$ENTRY_POINT={$ENTRY_POINT}" +
"&$STREAM_ID={$STREAM_ID}",
route = profileRoute(),
arguments = profileArguments(),
) {
ProfilePreview(
//profile params
)
}
После (концепт):
@Serializable
data class ProfilePreviewPointScreen(
override val deepLinkPath: String = "${ScreenName.PROFILE.value}"
) : NavKey,
DeepLinkable
entry<ProfilePreviewPointScreen>(
metadata = BottomSheetSceneStrategy.bottomSheet()
) { entry ->
ProfilePreview(
//profile params
)
}
На первый взгляд это может показаться не таким уж масштабным изменением, но на практике оно значительно упростило поддержку и отладку.
Стабилизация
Полное регрессионное тестирование, стабилизация и исправление ошибок заняли около полутора недель. Честно говоря, я ожидал, что мы будем находить ошибки и стабилизировать приложение ещё как минимум месяц — только не говорите об этом моему руководителю.
Благодаря автоматизированным тестам нам удалось обнаружить огромное количество проблем, которые было бы очень сложно воспроизвести вручную. При этом пользователи почти наверняка столкнулись бы с ними и написали бы об этом в отзывах.
Выпуск
Мы завершили стабилизацию, провели регрессионное тестирование, выпустили новую версию, открыли Crashlytics, развернули обновление для 1% пользователей, затаили дыхание и стали ждать.
Ждать пришлось недолго — начали поступать сообщения о сбоях.
Проблема с возвратной навигацией
Чтобы понять причину этих сбоев, нужно разобраться в работе возвратной навигации. Существует два варианта такого перехода.
- Обычная возвратная навигация. Находясь на текущем экране, пользователь нажимает кнопку «Назад» и возвращается на предыдущий экран.
- Возврат на два и более экрана назад. Например, после блокировки пользователя в чате его нужно вернуть сразу на несколько экранов назад.
Для первого варианта всё работало стабильно. Во втором случае после миграции мы передавали через навигацию параметр entryPoint: NavKey, чтобы точно знать, на какой экран нужно вернуть пользователя после блокировки.
data class BlockUserRoute( val entryPoint: NavKey )
Это приводило к сбою со следующей ошибкой: Fatal Exception: kotlinx.serialization.SerializationException
В Navigation 3 все параметры, передаваемые через навигацию, должны поддерживать сериализацию. Наш NavKey не был сериализуемым, поэтому пришлось создать обёртку:
@Serializable open class NavStateSerializable : NavKey
Мы выпустили срочное исправление, и со второй попытки обновление удалось полностью развернуть для всех пользователей.
Инициативность — это хорошо, но не всегда
Помните, я говорил, что выполнял всю миграцию вручную? Я пытался использовать искусственный интеллект, но ничего не получилось. Так вот, через полтора месяца после нашего выпуска Google представила специальные навыки для Claude Code, предназначенные для миграции на Navigation 3.
Мне стало интересно, насколько сильно Claude Code мог бы мне помочь. Я вернулся к ветке до миграции и попробовал выполнить переход с его помощью. Могу уверенно сказать, что это сэкономило бы мне примерно неделю работы. Иными словами, миграция прошла бы вдвое быстрее. Да, функции-расширения и устаревший код всё равно пришлось бы переносить вручную. Кроме того, много времени ушло бы на проверку изменений, внесённых Claude Code. Однако невозможно отрицать, что он автоматизировал бы огромную часть однообразной ручной работы, которую мне пришлось выполнить самостоятельно.
Кроме того, Google уже предлагает встроенное решение для нижних панелей — через SceneStrategy. Мы заменили собственный обходной вариант на BottomSheetSceneStrategy. Такой подход оказался значительно удобнее.
Полный код BottomSheetSceneStrategy можно найти здесь.
Таким образом, если бы я не торопился с миграцией и подождал ещё пару месяцев, переход оказался бы немного проще, а разработка благодаря Claude Code и готовому решению для нижних панелей заняла бы вдвое меньше времени. Впрочем, думаю, продолжительность стабилизации и исправления ошибок почти не изменилась бы.
Выводы и полученные уроки
Снова о нижних панелях
Поскольку нижние панели в нашем приложении находятся внутри навигационного стека, здесь есть один важный нюанс.
Внимательно рассмотрим следующую последовательность переходов:
Представим такой сценарий. Bottom Sheet, управляемый через навигацию, очевидно, является частью навигационного стека. Но если перейти с него на следующий экран и не закрыть панель вручную, то при нажатии кнопки «Назад» на новом экране нижняя панель снова появится, поскольку она всё ещё остаётся в стеке.
Поэтому перед переходом на другой экран нижнюю панель необходимо закрывать вручную. И об этом очень легко забыть.
Для этого всегда можно написать функцию-расширение, однако поведение всё равно может быть неочевидным — особенно для новых разработчиков в команде.
Когда я не рекомендовал бы миграцию
Я настоятельно рекомендую тщательно оценить необходимость перехода на Navigation 3 в следующих случаях.
- Архитектура на основе фрагментов. Перед миграцией придётся полностью отказаться от фрагментов. Это может потребовать масштабной переработки кода с высоким уровнем риска.
- Жёсткие сроки. Даже если вы уже используете навигацию в Compose, миграция затрагивает огромную часть приложения и может дорого обойтись бизнесу из-за регрессий.
- Собственная система навигации. Если ваше решение уже работает стабильно, переход может оказаться неоправданным.
- Игры. Если ваше приложение является игрой и строго зависит от определённой ориентации экрана — чаще всего альбомной, — Google разрешает сохранить фиксированную ориентацию в качестве исключения.
Преимущества
Несмотря на все возникшие проблемы, я считаю, что миграция была полностью оправданной и в итоге принесла нам пользу.
- Многооконный интерфейс. Адаптивные компоновки больше не являются для нас проблемой. Теперь их можно легко реализовать, просто указав подходящую
SceneStrategyдля конкретного экрана. - Предсказуемая навигация. Навигационный стек стал значительно прозрачнее, а его отладка — проще.
- Глубокие ссылки. Во время миграции я переработал большое количество устаревшего кода и привёл обработку глубоких ссылок к понятной структуре.
Итоги
Весь процесс миграции можно кратко описать следующим образом:
- Команда: один активный и ещё не выгоревший разработчик;
- Время разработки: две недели;
- Разрешение конфликтов при слиянии: один день;
- Затронутая область: 302 файла, то есть фактически всё приложение;
- Тестирование и стабилизация: около полутора недель;
- Срочных исправлений после выпуска: одно.
Что особенно важно, успешность миграции мы оценивали по нескольким практическим критериям:
- Стабильность навигационных сценариев после выпуска — количество сбоев и регрессий, связанных с навигацией;
- Скорость подключения новых экранов к новой системе навигации;
- Возможность создавать адаптивные компоновки без дополнительных обходных решений;
- Удобство отладки навигационного стека и воспроизведения проблемных сценариев.
После миграции наиболее заметные изменения проявились в повседневной разработке:
- Новые экраны стало проще подключать к навигации без дополнительных функций-расширений;
- Навигационный стек стал более предсказуемым, а его отладка — более удобной;
- Значительная часть устаревшей логики просто исчезла;
- Navigation 3 оказалась гораздо ближе к подходу, основанному на состоянии, а адаптивные компоновки больше не воспринимаются как отдельный масштабный проект.
Но если вы тоже планируете миграцию, закладывайте больше времени на стабилизацию, не недооценивайте сложности с нижними панелями и особенно внимательно проверяйте настройку сериализации навигационных параметров.
Надеюсь, мой опыт поможет вам избежать моих ошибок и сделает вашу миграцию быстрее и менее болезненной.

