1. Принципы навигации: NavHost, бэкстек и фиксация начального экрана
Принципы навигации: NavHost, бэкстек и фиксация начального экрана
Навигация в современном Android-приложении — это не просто переключение видимости View или замена фрагментов. В экосистеме Jetpack Compose навигация представляет собой реакцию на изменение состояния. Когда пользователь переходит от списка товаров к деталям заказа, он фактически изменяет состояние приложения, а UI перерисовывается, чтобы соответствовать этому новому состоянию.
Понимание фундаментальных принципов работы NavHost и внутреннего устройства стека возврата (back stack) — это то, что отличает профессионального разработчика от новичка, копирующего код из StackOverflow. Ошибки на этом этапе приводят к утечкам памяти, некорректному поведению кнопки «Назад» и потере данных при повороте экрана.
Архитектура единой Activity
Jetpack Compose поощряет подход Single Activity. Это означает, что ваше приложение состоит из одной Activity, которая служит контейнером для всего контента. Внутри этой Activity происходит подмена Composable-функций.
В отличие от классического подхода с множеством Activity или Fragment, где системой навигации управляла сама ОС Android (через Intent и FragmentManager), в Compose вы берете управление на себя. Вы создаете граф навигации, который определяет структуру вашего приложения.
Ключевые компоненты навигации
Для реализации навигации в Compose используются три основных компонента, которые работают в тесной связке:
NavController: Мозг операции
NavController — это объект, который хранит состояние навигации и бэкстек. Он отвечает за выполнение переходов, обработку нажатия кнопки «Назад» и управление глубокими ссылками (Deep Links). Этот объект должен быть создан на верхнем уровне иерархии вашего UI, обычно внутри MainActivity или корневой Composable-функции.
Для создания используется функция rememberNavController():
Этот объект переживает рекомпозицию, так как использует remember, и сохраняет состояние при повороте экрана благодаря rememberSaveable внутри своей реализации.
NavHost: Контейнер отображения
NavHost — это Composable-функция, которая связывает NavController с графом навигации. Она действует как «экран», на котором проецируется текущий пункт назначения (Destination). Когда вы командуете контроллеру перейти на другой экран, NavHost выполняет рекомпозицию и подменяет содержимое.
Пример базовой настройки согласно документации Android Developers:
В этом примере лямбда-выражение внутри NavHost использует NavGraphBuilder для определения графа. Функция composable связывает строковый маршрут (route) с конкретной Composable-функцией.
Бэкстек (Back Stack): Структура и поведение
Бэкстек — это структура данных, работающая по принципу LIFO (Last In, First Out — «последним пришёл, первым ушёл»). Это история перемещений пользователя.
Как работает стек
Представьте бэкстек как стопку тарелок. Каждая «тарелка» — это запись о посещенном экране (NavBackStackEntry). Эта запись хранит не только информацию о том, какой экран показать, но и состояние этого экрана (ViewModel, SavedStateHandle).
navController.navigate("details"), новый экран кладется поверх текущего. Предыдущий экран не уничтожается полностью, но переходит в фоновое состояние (stopped).navController.popBackStack(), верхняя «тарелка» снимается со стопки и уничтожается. Управление возвращается к экрану, который лежал под ней.Состояние навигации
Согласно официальным принципам навигации, состояние навигации всегда представлено стеком пунктов назначения. Верхняя часть стека — это текущий экран. Нижняя часть стека — это всегда начальный экран (Start Destination).
Важно понимать, что операции со стеком всегда происходят на его вершине. Вы либо кладете что-то сверху, либо убираете сверху. Исключение составляют сложные операции очистки стека (например, popUpTo), которые позволяют убрать сразу несколько «тарелок», чтобы вернуться к определенному состоянию.
Пример работы стека:
[Home][Home, List][Home, List, Details][Home, List] (Details уничтожен)Фиксация начального экрана (Start Destination)
Каждое приложение имеет фиксированную точку входа. Это первый экран, который видит пользователь при запуске приложения из лаунчера. В терминах Jetpack Navigation это называется Start Destination.
Почему это важно?
Начальный экран — это фундамент вашего бэкстека. Он загружается автоматически при создании графа и остается в самом низу стека до тех пор, пока пользователь не решит закрыть приложение.
Правила работы с начальным экраном:
Ошибка циклической навигации
Распространенная ошибка новичков — попытка «вернуться» на главный экран через navigate("home") вместо использования popBackStack.
Рассмотрим сценарий:
[Home]navigate("profile"). Стек: [Home, Profile]navigate("home").Результат: Стек становится [Home, Profile, Home]. Теперь у вас два экземпляра домашнего экрана в памяти. Если пользователь нажмет «Назад», он попадет в профиль, а затем снова домой. Это нарушает UX и расходует память.
Правильное решение: Использовать popUpTo для возврата к существующему экземпляру экрана или просто popBackStack, если экран находится прямо под текущим.
Маршруты (Routes) как идентификаторы
В классическом Jetpack Navigation (до версии 2.8.0) маршруты представлялись строками (String), напоминающими URL-адреса веб-сайтов. Например, "profile/user123".
Хотя сейчас набирает популярность типобезопасная навигация (Type-Safe Navigation) с использованием объектов и классов, принцип остается прежним: Route — это уникальный адрес пункта назначения. NavHost использует этот адрес, чтобы найти соответствующий composable в графе и отобразить его.
Как отмечается в статье на habr.com, при определении графа вы указываете route (адрес) и startDestination (точку входа). Это создает карту, по которой NavController будет перемещать пользователя.
Жизненный цикл Composable в навигации
Понимание жизненного цикла критически важно для управления ресурсами. Composable-функции, привязанные к навигации, имеют свой жизненный цикл, отличный от Activity.
* Вход (Enter): Когда экран добавляется в стек, вызывается Composable-функция. Запускаются все LaunchedEffect и DisposableEffect.
* Уход в фон (Stop): Когда поверх текущего экрана открывается новый, текущий экран не уничтожается, но его UI перестает отрисовываться. Однако его состояние (ViewModel) сохраняется в памяти.
* Уничтожение (Destroy): Когда экран удаляется из стека (пользователь нажал «Назад»), Composable покидает композицию. Срабатывает onDispose в DisposableEffect, а связанная с экраном ViewModel очищается (вызывается onCleared).
Это поведение обеспечивает эффективность: вы не держите в памяти тяжелые ресурсы закрытых экранов, но сохраняете мгновенный доступ к экранам, лежащим ниже в стеке.
Итоги
* NavHost и NavController — неразрывная пара. Контроллер управляет логикой и состоянием, а хост отображает актуальный экран в UI.
* Бэкстек работает по принципу LIFO. Новые экраны кладутся сверху, кнопка «Назад» снимает верхний экран. Нижний элемент стека — всегда начальный экран.
* Фиксация начального экрана обязательна. Это якорь вашего приложения. Нажатие «Назад» на этом экране должно выводить из приложения.
* Избегайте дублирования в стеке. Для возврата назад используйте popBackStack или навигацию с флагом popUpTo, чтобы не создавать копии экранов.
* Жизненный цикл привязан к стеку. Экраны, ушедшие из стека, уничтожаются вместе с их состоянием, что экономит ресурсы устройства.