Nested Navigation Graphs
RecipeBox's "Add Recipe" flow isn't a single screen — it's a short sequence: first enter the recipe's details (title, ingredients, steps, category, prep time), then confirm and save. Both steps belong to one self-contained flow that the rest of the app shouldn't need to know the inner structure of. Nested navigation graphs let you group a flow like this under a single route, so the outer app treats it as one destination while it internally manages its own multi-step back stack.
Why Group Destinations Into a Nested Graph?
Every destination you've registered so far — "recipes", "search",
"profile", "recipeDetail/{recipeId}" — sits directly in one flat
NavHost. That's fine while the app is small, but it starts to blur two different
kinds of navigation together:
- Top-level navigation — switching between Recipes, Search, and Profile from the bottom bar
- Flow-internal navigation — moving between the steps of adding a recipe, which is really one task with an internal back stack of its own
A nested graph makes that second kind explicit: the "Add Recipe" details screen and its confirm screen become their own mini back stack, reachable through a single entry point route, and the rest of the app only ever navigates to that one entry point — it doesn't need to know or care that there are two steps inside.
The navigation() Builder
Inside a NavHost's builder lambda, alongside composable() calls, you can
call navigation(startDestination, route) to declare a nested graph. Everything inside
its lambda is a normal composable() destination, but the whole group is now reachable
from outside as a single unit under route:
NavHost(navController = navController, startDestination = "recipes") {
composable("recipes") { RecipeListScreen(recipes = recipes, onAddClick = {
navController.navigate("addRecipeFlow")
}) }
navigation(startDestination = "addRecipeDetails", route = "addRecipeFlow") {
composable("addRecipeDetails") { /* step 1 */ }
composable("addRecipeConfirm") { /* step 2 */ }
}
}
Navigating to "addRecipeFlow" from the recipe list lands the user on
"addRecipeDetails" — the nested graph's own startDestination — exactly as
if "addRecipeFlow" were an ordinary screen.
Worked Example: RecipeBox's Add Recipe Flow
A complete nested graph for RecipeBox's two-step Add Recipe flow, with data carried from the
details step to the confirm step through a shared ViewModel scoped to the nested
graph itself:
NavHost(navController = navController, startDestination = "recipes") {
composable("recipes") {
RecipeListScreen(
recipes = recipes,
onAddClick = { navController.navigate("addRecipeFlow") }
)
}
navigation(
startDestination = "addRecipeDetails",
route = "addRecipeFlow"
) {
composable("addRecipeDetails") { backStackEntry ->
val parentEntry = remember(backStackEntry) {
navController.getBackStackEntry("addRecipeFlow")
}
val addRecipeViewModel: AddRecipeViewModel = viewModel(parentEntry)
AddRecipeDetailsScreen(
viewModel = addRecipeViewModel,
onNextClick = { navController.navigate("addRecipeConfirm") }
)
}
composable("addRecipeConfirm") { backStackEntry ->
val parentEntry = remember(backStackEntry) {
navController.getBackStackEntry("addRecipeFlow")
}
val addRecipeViewModel: AddRecipeViewModel = viewModel(parentEntry)
AddRecipeConfirmScreen(
viewModel = addRecipeViewModel,
onSaveClick = {
addRecipeViewModel.save()
navController.popBackStack("recipes", inclusive = false)
}
)
}
}
}
The navController.getBackStackEntry("addRecipeFlow") trick is the key detail: instead
of scoping AddRecipeViewModel to each individual screen (which would lose the
in-progress recipe when moving from the details step to the confirm step), both steps grab the
same ViewModel instance scoped to the nested graph's own back stack
entry. The half-filled-in recipe survives the step transition, and once the whole
"addRecipeFlow" graph is popped off the back stack, that ViewModel — and
the in-progress draft it was holding — is cleared automatically.
popBackStack("recipes", inclusive = false) on save pops everything down to, but not
including, the "recipes" destination — clearing both Add Recipe steps off the back
stack in one call, so pressing back from the list afterward doesn't walk the user back through the
flow they just completed.