Irbisa · Cheatsheet
Android Navigation
Navigation Component, Safe Args, type-safe routes, deep links, back stack, predictive back
In a single-Activity app with a
NavHost, what happens when the user presses Back, and which back stack pops?What is the difference between Up and Back?
Why should you pass an id rather than the whole object as a navigation argument?
How do you pass arguments between destinations: Safe Args versus
@Serializableroutes?// 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)) }) }…What do
popUpTo,inclusiveandlaunchSingleTopdo, and how do you remove the login flow from the back stack after sign-in?@Serializable data object AuthGraph@Serializable data object Login@Serializable data object Otp@Serializable data object Home NavHost(navController, startDestination = AuthGraph) {…What is a nested navigation graph, and how do you scope a ViewModel to it?
@Serializable data object Home@Serializable data object WizardGraph@Serializable data object Step1@Serializable data object Step2 data class Draft(val text: String = "")…How do you return a result from a destination to the previous one?
// 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…Find the bug: after the user picks a city,
CityPickercloses, 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) }) }}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) } ) }}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()}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)) } ) } }}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?
@Serializable data object Home@Serializable data object OrdersGraph@Serializable data object OrderList@Serializable data class OrderDetail(val id: Long) NavHost(navController, startDestination = Home) {…What is the difference between a deep link and an Android App Link, and how is an App Link verified?
<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"…A
NavBackStackEntryis aLifecycleOwner, aViewModelStoreOwnerand aSavedStateRegistryOwner. What does that mean for screen-scoped state?Single Activity or multiple Activities: which do you choose in 2026, and when does a second Activity make sense?
What is predictive back, and what do you change in an existing app to support 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…How do you test navigation in a Compose app?
// 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)…In a multi-module app, how does
:feature:checkoutopen a screen from:feature:profilewithout depending on it?// :feature:checkout - knows nothing about :feature:profile@Serializable data object Checkout fun NavGraphBuilder.checkoutSection(onOpenProfile: (Long) -> Unit) { composable<Checkout> { CheckoutScreen(onOpenProfile = onOpenProfile) }}…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?
// In the NavHost the whole screen is ONE destination: composable<Inbox> { MailScreen() }@Composablefun MailScreen() { val panes = rememberListDetailPaneScaffoldNavigator<Long>() // pane history + selection val scope = rememberCoroutineScope()…What is Navigation 3, and what changes compared with
NavController?@Serializable data object Home : NavKey@Serializable data object Login : NavKey@Serializable data class Article(val id: Long) : NavKey @Composablefun App(loggedIn: Boolean) {…