Phase 3 · Navigation & ArchitectureModule 15~30 min read

Navigation in Compose

Move between screens with Navigation Compose, routes, arguments, and the back stack.

What you'll learn

Real apps have many screens, and users move between them and press Back. Navigation Compose is the official library for that: you declare your screens and the routes between them, and it manages the back stack for you. This module gets you navigating confidently.

By the end you'll be able to:

  • Set up a NavController and NavHost
  • Define routes and navigate between screens
  • Pass arguments from one screen to another
  • Understand and control the back stack

NavController & NavHost

Navigation has two core pieces:

  • NavController — the brain. It knows the current screen and the history, and you call navigate(...) on it. Create one with rememberNavController().
  • NavHost — a composable that maps each route (a string) to a screen. It always shows the destination that matches the controller's current route.
AppNavHost.kt
// build.gradle (app):  implementation("androidx.navigation:navigation-compose:2.7.x")

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

    NavHost(navController = navController, startDestination = "home") {
        composable("home") {
            HomeScreen(onOpenSettings = { navController.navigate("settings") })
        }
        composable("settings") {
            SettingsScreen(onBack = { navController.popBackStack() })
        }
    }
}

Key idea

Notice the screens don't hold the NavController — the NavHost passes them callbacks like onOpenSettings. This keeps HomeScreen stateless and testable (it doesn't know navigation exists), exactly the "events up" pattern from Module 9. Navigation is just another event.

Each composable("route") { ... } block registers one destination. To move, call navController.navigate("route"). The startDestination is the screen shown first. Routes are plain strings — keep them in constants to avoid typos, or use the newer type-safe navigation (next module).

Passing arguments

Screens often need data — a detail screen needs to know which item. Encode the argument in the route with a placeholder like "detail/{productId}", declare its type, and read it from the destination's arguments:

ProductNav.kt
NavHost(navController, startDestination = "list") {
    composable("list") {
        ProductList(onOpen = { id -> navController.navigate("detail/$id") })
    }
    composable(
        route = "detail/{productId}",
        arguments = listOf(navArgument("productId") { type = NavType.IntType }),
    ) { backStackEntry ->
        val id = backStackEntry.arguments?.getInt("productId") ?: 0
        ProductDetail(productId = id)
    }
}

Tip

Pass identifiers, not objects. Send a small productId and let the destination look up the full data (from a ViewModel or repository). Routes are strings in a URL-like format, so they're a poor place for large or complex objects.

The back stack

Navigation keeps a back stack: each navigate pushes a screen on top, and the system Back button (or popBackStack()) pops it off, returning to the previous one. Usually this is automatic and exactly what you want. Sometimes you need to shape it — after logging in, you don't want Back to return to the login screen:

BackStack.kt
// Go back one screen (same as the system back button):
navController.popBackStack()

// Navigate and clear intermediate screens (e.g. after login):
navController.navigate("home") {
    popUpTo("login") { inclusive = true }   // remove login from the stack
    launchSingleTop = true                    // don't stack duplicates
}
  • popUpTo("login") { inclusive = true } removes screens up to and including login before navigating — so Back won't revisit it.
  • launchSingleTop = true avoids pushing a duplicate if you're already on that screen (handy for bottom-nav taps).

Recap & quick check

Key takeaways

  • Navigation Compose declares screens and routes; it manages the back stack for you.
  • NavController is the brain (rememberNavController()); NavHost maps routes to screen composables.
  • navigate("route") moves forward; startDestination is the first screen shown.
  • Pass small arguments in the route (detail/{id}), declare their type, and read them from the destination.
  • Shape the back stack with popBackStack(), popUpTo(...) { inclusive }, and launchSingleTop.

Quick check

1. Which piece maps routes to screen composables?

2. How do you move to another screen?

3. Best way to pass which item a detail screen should show?

4. After login, how do you stop Back from returning to the login screen?

5. Why give screens callbacks instead of the NavController?

You can move between screens. Next we build real navigation patterns — bottom tabs, nested graphs, and type-safe routes.