Navigation Compose Basics

Every RecipeBox screen so far has lived alone — a single composable, no way to move to another screen. A real app is a graph of screens: tap a recipe in the list, and you expect to land on its detail view, with a back button that returns you to the list. Navigation Compose is the Jetpack library that manages exactly that — the back stack of destinations and the transitions between them — entirely in Compose, with no Activity or Fragment per screen.

The Three Building Blocks

Navigation Compose is built from three pieces that work together:

PieceRole
NavControllerThe object that actually holds the back stack and performs navigation — you call navController.navigate(...) on it
NavHostThe composable that hosts the currently-active destination and swaps it out as the NavController's back stack changes
composable("route") { }Registers one destination inside the NavHost, under a given route string, with the composable content to show for it

Creating a NavController

A NavController is created with rememberNavController(), typically once, near the top of your app, and then passed down (or hoisted) to wherever navigation needs to be triggered:

@Composable
fun RecipeBoxApp() {
    val navController = rememberNavController()
    // navController is now ready to be handed to a NavHost
}

NavHost and Routes

NavHost ties a NavController to a set of possible destinations. Each destination is registered with composable(route), where route is just a plain string that identifies that screen — you choose the string, Navigation Compose doesn't attach any special meaning to it beyond using it as a lookup key:

@Composable
fun RecipeBoxApp() {
    val navController = rememberNavController()

    NavHost(
        navController = navController,
        startDestination = "recipeList"
    ) {
        composable("recipeList") {
            RecipeListScreen(onRecipeClick = { recipeId ->/* navigate */ })
        }
        composable("recipeDetail") {
            RecipeDetailScreen()
        }
    }
}

startDestination tells the NavHost which route to show first — here, RecipeBox opens straight to the recipe list.

Navigating Between Screens

To move to another destination, call navController.navigate("route") from wherever the navigation should be triggered — typically a click handler passed down into a screen composable:

composable("recipeList") {
    RecipeListScreen(
        onRecipeClick = {
            navController.navigate("recipeDetail")
        }
    )
}

Every call to navigate() pushes a new entry onto the back stack. The system back button (or a back arrow you wire up yourself with navController.popBackStack()) pops the top entry off, returning to whatever was there before — you get this back-stack behavior for free, without managing a FragmentManager transaction yourself.

Worked Example: RecipeBox List & Detail

A complete, minimal two-screen RecipeBox setup: a recipe list that navigates to a detail screen, and a detail screen with its own back button:

@Composable
fun RecipeBoxApp(recipes: List<Recipe>) {
    val navController = rememberNavController()

    NavHost(navController = navController, startDestination = "recipeList") {
        composable("recipeList") {
            RecipeListScreen(
                recipes = recipes,
                onRecipeClick = { navController.navigate("recipeDetail") }
            )
        }
        composable("recipeDetail") {
            RecipeDetailScreen(
                onBackClick = { navController.popBackStack() }
            )
        }
    }
}

@Composable
fun RecipeListScreen(recipes: List<Recipe>, onRecipeClick: (String) -> Unit) {
    LazyColumn {
        items(items = recipes, key = { it.id }) { recipe ->
            RecipeRow(
                recipe = recipe,
                modifier = Modifier.clickable { onRecipeClick(recipe.id) }
            )
        }
    }
}

@Composable
fun RecipeDetailScreen(onBackClick: () -> Unit) {
    Column(modifier = Modifier.padding(16.dp)) {
        TextButton(onClick = onBackClick) {
            Text("Back")
        }
        Text("Recipe details go here", style = MaterialTheme.typography.titleLarge)
    }
}

Notice that RecipeDetailScreen above doesn't yet know which recipe to show — it has no way to receive the clicked recipe's ID. That's the very next thing to fix.

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: Passing Arguments Between Screens fixes exactly this gap — getting the clicked recipe's ID from the list screen into the detail screen's route.