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:
| Piece | Role |
|---|---|
NavController | The object that actually holds the back stack and performs navigation — you call navController.navigate(...) on it |
NavHost | The 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.