Принципы навигации: 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 — центральный координатор.
- NavGraph — карта маршрутов.
- NavHost — контейнер для отображения.
NavController: Мозг операции
NavController — это объект, который хранит состояние навигации и бэкстек. Он отвечает за выполнение переходов, обработку нажатия кнопки «Назад» и управление глубокими ссылками (Deep Links). Этот объект должен быть создан на верхнем уровне иерархии вашего UI, обычно внутри MainActivity или корневой Composable-функции.
Для создания используется функция rememberNavController():
val navController = rememberNavController()
Этот объект переживает рекомпозицию, так как использует remember, и сохраняет состояние при повороте экрана благодаря rememberSaveable внутри своей реализации.
NavHost: Контейнер отображения
NavHost — это Composable-функция, которая связывает NavController с графом навигации. Она действует как «экран», на котором проецируется текущий пункт назначения (Destination). Когда вы командуете контроллеру перейти на другой экран, NavHost выполняет рекомпозицию и подменяет содержимое.
Пример базовой настройки согласно документации Android Developers:
NavHost(navController = navController, startDestination = "profile") {
composable("profile") { ProfileScreen() }
composable("friendslist") { FriendsListScreen() }
}
В этом примере лямбда-выражение внутри NavHost использует NavGraphBuilder для определения графа. Функция composable связывает строковый маршрут (route) с конкретной Composable-функцией.
Бэкстек (Back Stack): Структура и поведение
Бэкстек — это структура данных, работающая по принципу LIFO (Last In, First Out — «последним пришёл, первым ушёл»). Это история перемещений пользователя.
Как работает стек
Представьте бэкстек как стопку тарелок. Каждая «тарелка» — это запись о посещенном экране (NavBackStackEntry). Эта запись хранит не только информацию о том, какой экран показать, но и состояние этого экрана (ViewModel, SavedStateHandle).
- Push (Добавление): Когда вы вызываете
navController.navigate("details"), новый экран кладется поверх текущего. Предыдущий экран не уничтожается полностью, но переходит в фоновое состояние (stopped). - Pop (Удаление): Когда пользователь нажимает системную кнопку «Назад» или вы вызываете
navController.popBackStack(), верхняя «тарелка» снимается со стопки и уничтожается. Управление возвращается к экрану, который лежал под ней.
Состояние навигации
Согласно официальным принципам навигации, состояние навигации всегда представлено стеком пунктов назначения. Верхняя часть стека — это текущий экран. Нижняя часть стека — это всегда начальный экран (Start Destination).
Важно понимать, что операции со стеком всегда происходят на его вершине. Вы либо кладете что-то сверху, либо убираете сверху. Исключение составляют сложные операции очистки стека (например, popUpTo), которые позволяют убрать сразу несколько «тарелок», чтобы вернуться к определенному состоянию.
Пример работы стека:
- Запуск приложения: Стек =
[Home] - Переход к списку: Стек =
[Home, List] - Переход к деталям: Стек =
[Home, List, Details] - Нажатие «Назад»: Стек =
[Home, List](Details уничтожен)
Фиксация начального экрана (Start Destination)
Каждое приложение имеет фиксированную точку входа. Это первый экран, который видит пользователь при запуске приложения из лаунчера. В терминах Jetpack Navigation это называется Start Destination.
Почему это важно?
Начальный экран — это фундамент вашего бэкстека. Он загружается автоматически при создании графа и остается в самом низу стека до тех пор, пока пользователь не решит закрыть приложение.
Правила работы с начальным экраном:
- Единственность: У каждого графа навигации должен быть ровно один стартовый маршрут.
- Точка выхода: Если пользователь находится на стартовом экране и нажимает «Назад», приложение должно закрыться (или свернуться). Это ожидаемое системное поведение Android.
- Восстановление: При восстановлении процесса (например, после того как система убила приложение для освобождения памяти) навигационный компонент восстановит стек, и начальный экран снова окажется в основании.
Ошибка циклической навигации
Распространенная ошибка новичков — попытка «вернуться» на главный экран через 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, чтобы не создавать копии экранов. - Жизненный цикл привязан к стеку. Экраны, ушедшие из стека, уничтожаются вместе с их состоянием, что экономит ресурсы устройства.