Irbisa · Cheatsheet

Android Navigation

Navigation Component, Safe Args, type-safe routes, deep links, back stack, predictive back

Middle Developer · 20 questions

  1. 01

    In a single-Activity app with a NavHost, what happens when the user presses Back, and which back stack pops?

    The NavHost registers an OnBackPressedCallback that is enabled while its back stack holds more than one entry, so Back pops the NavController's stack first; only at the start destination does Back fall through and finish the Activity.

  2. 02

    What is the difference between Up and Back?

    Back is chronological: it reverses the user's last step and can leave your app.

  3. 03

    Why should you pass an id rather than the whole object as a navigation argument?

    An argument is a copy frozen into a Bundle at navigation time, so a whole object goes stale, eats into the saved-state size limit and cannot come from a deep link.

  4. 04

    How do you pass arguments between destinations: Safe Args versus @Serializable routes?

    For XML graphs the Safe Args Gradle plugin generates Directions and Args classes from <argument> tags; since Navigation 2.8 you can instead declare a @Serializable route class, navigate with an instance of it and read it back with 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

    What do popUpTo, inclusive and launchSingleTop do, and how do you remove the login flow from the back stack after sign-in?

    popUpTo(X) pops everything above X before navigating, inclusive = true pops X as well, and launchSingleTop reuses the destination if it is already on top.

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

    What is a nested navigation graph, and how do you scope a ViewModel to it?

    A nested graph groups destinations into a flow with its own start destination, and it sits on the back stack as an entry of its own.

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

    How do you return a result from a destination to the previous one?

    Write the value into previousBackStackEntry.savedStateHandle and pop; the caller observes its own entry's 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

    Find the bug: after the user picks a city, CityPicker closes, but the search screen still shows no city.

    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)    })  }}

    The result is written after popBackStack().

  9. 09

    Users say Back walks through every tab they tapped, and each tab forgets its scroll and depth. What is wrong with this bottom bar?

    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) }    )  }}

    Every tap is a plain navigate, so tabs pile up on one back stack and nothing is saved.

  10. 10

    This screen sometimes puts Login on the back stack twice. Why, and where should the login check live?

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

    A composable body can run many times (recomposition, transition frames), so a navigate() written there fires repeatedly and stacks Login.

  11. 11

    Find the bug: a quick double tap on an order opens its detail twice, so the first Back shows the same order again.

    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)) }      )    }  }}

    Nothing stops a second tap: during the exit transition the list is still on screen and clickable, so a double tap calls navigate() twice and pushes OrderDetail twice.

  12. 12

    A notification deep link opens order detail, and Back goes to Home (or leaves the app) instead of the order list. How do you fix the back stack?

    When a deep link starts a new task, Navigation builds the back stack from the graph hierarchy: the start destination of each parent graph.

    @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

    What is the difference between a deep link and an Android App Link, and how is an App Link verified?

    A deep link is any URI your intent filter matches, so the user may get a chooser.

    <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

    A NavBackStackEntry is a LifecycleOwner, a ViewModelStoreOwner and a SavedStateRegistryOwner. What does that mean for screen-scoped state?

    Each destination on the stack gets its own lifecycle, ViewModel store and saved state, all owned by its entry.

  15. 15

    Single Activity or multiple Activities: which do you choose in 2026, and when does a second Activity make sense?

    Single Activity is the default: one NavHost owns the whole back stack, so transitions, shared ViewModels and deep links live in one place.

  16. 16

    What is predictive back, and what do you change in an existing app to support it?

    Predictive back shows a preview of where Back leads while the user is still swiping, so the system must know before the gesture starts whether your app will handle it.

    // 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

    How do you test navigation in a Compose app?

    Keep screens unaware of the NavController (they take lambdas like onOpenOrder: (Long) -> Unit) and test them by asserting the lambda.

    // 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

    In a multi-module app, how does :feature:checkout open a screen from :feature:profile without depending on it?

    Only :app should see every feature: each feature exposes a NavGraphBuilder extension and takes outgoing navigation as lambdas, and :app wires the lambdas.

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

    On a tablet the list and the detail are visible side by side. Why is pushing the detail as its own destination wrong, and what do you use instead?

    A linear back stack assumes one visible 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

    What is Navigation 3, and what changes compared with NavController?

    Navigation 3 (stable since late 2025) makes the back stack a snapshot-state list of keys that you own and mutate, and NavDisplay renders whatever is in it.

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