Nested Navigation and the Back Stack
As an app grows past a handful of screens, a single flat list of destinations inside one NavHost becomes hard to reason about — an onboarding flow, a checkout flow, and the main app tabs are conceptually separate journeys that happen to share the same NavController. Nested navigation graphs let you group related destinations together as a self-contained sub-graph with its own start destination, while still living inside the same overall back stack managed by one NavController.
Cricket analogy: A flat NavHost for a growing app is like running a whole cricket board's fixtures, domestic league, international tour, and youth academy, off one undifferentiated calendar instead of separate, self-contained tournament structures under one governing body.
A nested graph is declared with navigation(startDestination = ..., route = "auth") { ... } inside the NavHost builder, containing its own composable() entries for login, signup, and forgot-password screens, for example. From the outside, the entire nested graph is reachable by navigating to its route ("auth"), and internally its screens navigate among themselves using the same NavController, unaware that they're nested.
Cricket analogy: A nested graph declared with route 'auth' is like a qualifying tournament bracket nested inside the larger world cup structure, teams navigate among their own qualifier matches, unaware they're just one sub-bracket of the bigger event.
NavHost(navController = navController, startDestination = "main") {
navigation(startDestination = "login", route = "auth") {
composable("login") {
LoginScreen(
onLoginSuccess = {
navController.navigate("main") {
popUpTo("auth") { inclusive = true }
}
},
onSignUp = { navController.navigate("signup") }
)
}
composable("signup") {
SignUpScreen(onBack = { navController.popBackStack() })
}
}
composable("main") {
MainScreen()
}
}Controlling the back stack with popUpTo
The example above uses popUpTo("auth") { inclusive = true } when navigating from login success to the main screen. Without it, a successful login would simply push 'main' on top of the auth flow's back stack entries, meaning pressing the system back button from 'main' would return the user to the login screen — clearly wrong once authenticated. popUpTo with inclusive = true clears every entry up to and including the auth graph's start, so main becomes the new base of the stack and there's nothing to 'go back' to.
Cricket analogy: popUpTo with inclusive true after login is like a team being promoted out of the qualifying rounds entirely, once you're in the main draw, there's no 'back button' to the qualifiers because that bracket was cleared from the tournament path.
Nested graph ViewModel scoping
One major benefit of nested graphs beyond visual organization is ViewModel scoping: a ViewModel obtained with hiltViewModel() inside a screen that specifies the nested graph's route as its viewModelStoreOwner is shared by every screen inside that graph and is cleared automatically when the entire graph is popped off the back stack. This is exactly the mechanism used to share, for example, sign-up form state across a multi-step signup flow without a global singleton.
Cricket analogy: ViewModel scoping to a nested graph is like a specific tour's team management staff, coach, physio, analyst, shared by every match in that tour and dismissed automatically once the tour ends, rather than a permanent board-wide staff.
Think of a nested graph as a 'chapter' within a book (the whole back stack). Screens within a chapter can flip back and forth freely, but popUpTo with inclusive = true is like tearing out the entire chapter once it's done, so the reader can't accidentally flip back into it.
A common bug is forgetting inclusive = true (or forgetting popUpTo entirely) after a flow like login or onboarding completes, leaving stale screens on the back stack that the user can navigate back into after the flow is 'done' — e.g. pressing back after login re-shows the login screen.
saveState and restoreState for tab-like navigation
When switching between top-level destinations (like bottom navigation tabs), popUpTo(startRoute) { saveState = true } combined with launchSingleTop = true and restoreState = true on the navigate call preserves each tab's own back stack and scroll position when switching away and back, rather than resetting the tab to its start destination every time it's revisited.
Cricket analogy: Preserving each tab's back stack with saveState and restoreState is like a multi-format player keeping their Test technique intact even after playing a T20 innings, switching formats doesn't reset their form back to zero each time.
- Nested graphs group related destinations (e.g. an auth or checkout flow) under a single route while sharing one overall NavController.
- navigation(startDestination, route) declares a sub-graph inside the NavHost builder.
- popUpTo with inclusive = true removes back stack entries up to and including a given destination, preventing 'back' from re-entering a completed flow.
- ViewModels scoped to a nested graph's route are shared across its screens and cleared when the graph is popped.
- Forgetting popUpTo after a completed flow is a common bug that leaves stale screens reachable via back.
- saveState/restoreState with launchSingleTop preserves each tab's back stack when navigating between top-level tab destinations.
Practice what you learned
1. What does declaring `navigation(startDestination = "login", route = "auth") { ... }` accomplish?
2. Why is `popUpTo("auth") { inclusive = true }` used when navigating from a successful login to the main screen?
3. What common bug results from forgetting to use popUpTo after a completed onboarding or login flow?
4. What is a key benefit of scoping a ViewModel to a nested navigation graph's route?
5. What combination of navigation options is typically used to preserve each tab's back stack in a bottom-navigation UI?
Was this page helpful?
You May Also Like
Navigation Compose Basics
An introduction to the Navigation component's Compose integration — NavController, NavHost, and route-based destinations for moving between screens.
Bottom Navigation and Tabs
Building persistent bottom navigation bars and swipeable tab layouts in Compose using NavigationBar, TabRow, and coordinated back stack state.
Passing Arguments Between Screens
How to pass IDs, primitives, and complex data safely between destinations in Navigation Compose using route arguments and shared ViewModels.
ViewModel Basics
What Android's ViewModel class does, how it survives configuration changes, and how to create and scope ViewModels correctly in a Compose app.