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.

Note: As throughout this track, this is correct, static Kotlin/Compose code — this website's own project doesn't run a live Android Studio build, so it isn't executing on this page.
Next up: Scaffold & TopAppBar moves on to the UI components track — starting with the screen-level scaffolding that ties a top bar, bottom bar, and content together.