Irbisa · Шпаргалка

Навигация в Android

Navigation Component, Safe Args, типизированные маршруты, deep links, back stack, predictive back

Middle-разработчик · 20 вопросов

  1. 01

    В приложении с одной Activity и NavHost пользователь нажимает «Назад». Что происходит и с какого стека снимается экран?

    NavHost регистрирует OnBackPressedCallback, который включён, пока в его стеке больше одной записи, поэтому «Назад» сначала снимает верхний экран со стека NavController; только на стартовом destination нажатие проходит дальше и завершает Activity.

  2. 02

    Чем Up отличается от Back?

    Back хронологический: он отменяет последний шаг пользователя и может вывести из приложения.

  3. 03

    Почему в аргумент навигации передают id, а не весь объект?

    Аргумент хранится как копия, замороженная в Bundle в момент перехода, поэтому целый объект устаревает, съедает лимит сохранённого состояния и не может прийти из deep link.

  4. 04

    Как передать аргументы между destination: Safe Args или @Serializable-маршруты?

    Для XML-графов Gradle-плагин Safe Args генерирует классы Directions и Args из тегов <argument>; начиная с Navigation 2.8 можно вместо этого объявить @Serializable-класс маршрута, переходить с его экземпляром и читать обратно через toRoute().

    // Type-safe routes (Navigation 2.8+)@Serializable data object Home@Serializable data class Detail(val orderId: Long, val title: String? = null) NavHost(navController, startDestination = Home) {  composable<Home> { HomeScreen(onOpen = { id -> navController.navigate(Detail(id)) }) }…
  5. 05

    Что делают popUpTo, inclusive и launchSingleTop, и как убрать экраны логина из стека после входа?

    popUpTo(X) перед переходом снимает со стека всё, что выше X, inclusive = true снимает и сам X, а launchSingleTop переиспользует destination, если он уже наверху.

    @Serializable data object AuthGraph@Serializable data object Login@Serializable data object Otp@Serializable data object Home NavHost(navController, startDestination = AuthGraph) {…
  6. 06

    Что такое вложенный граф навигации и как привязать ViewModel к нему?

    Вложенный граф объединяет destination в один флоу со своим стартовым экраном и сам лежит в стеке отдельной записью.

    @Serializable data object Home@Serializable data object WizardGraph@Serializable data object Step1@Serializable data object Step2 data class Draft(val text: String = "")…
  7. 07

    Как вернуть результат с экрана на предыдущий?

    Запишите значение в previousBackStackEntry.savedStateHandle и сделайте pop; вызывающий экран наблюдает за SavedStateHandle своей записи.

    // Picker: write into the caller's handle, then popcomposable<Picker> {  AddressList(onPick = { address ->    navController.previousBackStackEntry      ?.savedStateHandle      ?.set("picked_address_id", address.id)   // an id, never the object…
  8. 08

    Найдите баг: после выбора города CityPicker закрывается, но экран поиска так и не показывает город.

    NavHost(navController, startDestination = Home) {  composable<Home> { HomeScreen(onSearch = { navController.navigate(Search) }) }  composable<Search> { entry ->    val cityId by entry.savedStateHandle      .getStateFlow<Long?>("city_id", null)      .collectAsStateWithLifecycle()    SearchScreen(cityId, onPickCity = { navController.navigate(CityPicker) })  }  composable<CityPicker> {    CityList(onPick = { city ->      navController.popBackStack()      navController.previousBackStackEntry        ?.savedStateHandle?.set("city_id", city.id)    })  }}

    Результат записывается после popBackStack().

  9. 09

    Пользователи жалуются: «Назад» проходит по всем нажатым вкладкам, а каждая вкладка забывает скролл и глубину. Что не так с этим нижним меню?

    val current by navController.currentBackStackEntryAsState() NavigationBar {  tabs.forEach { tab ->    NavigationBarItem(      selected = current?.destination?.hasRoute(tab.route::class) == true,      onClick = { navController.navigate(tab.route) },      icon = { Icon(tab.icon, contentDescription = null) },      label = { Text(tab.label) }    )  }}

    Каждый тап вызывает обычный navigate, поэтому вкладки складываются в один стек и ничего не сохраняется.

  10. 10

    Этот экран иногда кладёт Login в стек дважды. Почему, и где должна жить проверка логина?

    @Composablefun ProfileScreen(loggedIn: Boolean, navController: NavController) {  if (!loggedIn) {    navController.navigate(Login)  }  ProfileContent()}

    Тело composable может выполняться много раз (рекомпозиция, кадры анимации), поэтому navigate(), написанный там, срабатывает снова и снова и копит Login в стеке.

  11. 11

    Найдите баг: быстрый двойной тап по заказу открывает его детали дважды, и первое «Назад» снова показывает тот же заказ.

    composable<Orders> {  val vm: OrdersViewModel = hiltViewModel()  val orders by vm.orders.collectAsStateWithLifecycle()  LazyColumn {    items(orders, key = { it.id }) { order ->      OrderRow(        order = order,        onClick = { navController.navigate(OrderDetail(order.id)) }      )    }  }}

    Второй тап ничто не останавливает: во время анимации выхода список ещё на экране и кликабелен, поэтому двойной тап вызывает navigate() дважды и кладёт OrderDetail в стек два раза.

  12. 12

    Deep link из уведомления открывает детали заказа, а «Назад» ведёт на Home (или закрывает приложение) вместо списка заказов. Как исправить стек?

    Когда deep link запускает новую задачу, Navigation строит стек по иерархии графа: из стартовых destination родительских графов.

    @Serializable data object Home@Serializable data object OrdersGraph@Serializable data object OrderList@Serializable data class OrderDetail(val id: Long) NavHost(navController, startDestination = Home) {…
  13. 13

    Чем deep link отличается от Android App Link и как App Link проходит верификацию?

    Deep link означает любой URI, который совпал с вашим intent-filter, поэтому пользователь может увидеть выбор приложения.

    <activity android:name=".MainActivity" android:exported="true">  <intent-filter android:autoVerify="true">    <action android:name="android.intent.action.VIEW" />    <category android:name="android.intent.category.DEFAULT" />    <category android:name="android.intent.category.BROWSABLE" />    <data android:scheme="https"…
  14. 14

    NavBackStackEntry одновременно реализует LifecycleOwner, ViewModelStoreOwner и SavedStateRegistryOwner. Что это значит для состояния экрана?

    У каждого destination в стеке свой жизненный цикл, своё хранилище ViewModel и своё сохранённое состояние, и всем этим владеет его запись.

  15. 15

    Одна Activity или несколько: что выбрать в 2026 году и когда вторая Activity оправдана?

    По умолчанию берите одну Activity: один NavHost владеет всем стеком, поэтому переходы, общие ViewModel и deep links живут в одном месте.

  16. 16

    Что такое predictive back и что нужно поменять в существующем приложении для его поддержки?

    Predictive back показывает превью того, куда ведёт «Назад», пока пользователь ещё ведёт пальцем, поэтому система должна знать до начала жеста, обработает ли его приложение.

    // wrong: the system cannot know in advance that Back is handled hereoverride fun onBackPressed() {  if (hasUnsavedChanges) showDiscardDialog() else super.onBackPressed()} // right: state sets isEnabled before the gesture starts…
  17. 17

    Как тестировать навигацию в Compose-приложении?

    Экраны не должны знать про NavController: они принимают лямбды вроде onOpenOrder: (Long) -> Unit, и тест проверяет вызов лямбды.

    // The screen: assert the callback, no NavController involved@Test fun tappingRowReportsOrderId() {  var opened: Long? = null  composeRule.setContent { OrderList(orders = sample, onOpenOrder = { opened = it }) }  composeRule.onNodeWithText("Order #42").performClick()  assertEquals(42L, opened)…
  18. 18

    Как в многомодульном приложении :feature:checkout открывает экран из :feature:profile, не завися от него?

    Все фичи должен видеть только :app: каждая фича отдаёт расширение NavGraphBuilder и принимает исходящую навигацию лямбдами, а :app их связывает.

    // :feature:checkout - knows nothing about :feature:profile@Serializable data object Checkout fun NavGraphBuilder.checkoutSection(onOpenProfile: (Long) -> Unit) {  composable<Checkout> { CheckoutScreen(onOpenProfile = onOpenProfile) }}…
  19. 19

    На планшете список и детали видны рядом. Почему пушить детали отдельным destination неправильно и что использовать вместо этого?

    Линейный стек предполагает один видимый destination.

    // In the NavHost the whole screen is ONE destination: composable<Inbox> { MailScreen() }@Composablefun MailScreen() {  val panes = rememberListDetailPaneScaffoldNavigator<Long>()   // pane history + selection  val scope = rememberCoroutineScope()…
  20. 20

    Что такое Navigation 3 и что меняется по сравнению с NavController?

    Navigation 3 (стабильна с конца 2025 года) делает стек снапшот-списком ключей, которым вы владеете и который сами меняете, а NavDisplay рисует то, что в нём лежит.

    @Serializable data object Home : NavKey@Serializable data object Login : NavKey@Serializable data class Article(val id: Long) : NavKey @Composablefun App(loggedIn: Boolean) {…