← All courses

Navigation in Compose

🗓 May 31, 2026 ⏱ 2 min read

One activity, many composable screens

Compose apps usually have a single Activity and navigate between composable “destinations” using Navigation-Compose. You define a NavHost listing your screens, and a NavController moves between them.

Setting up

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

    NavHost(navController, startDestination = "home") {
        composable("home") {
            HomeScreen(onOpenProfile = { id ->
                navController.navigate("profile/$id")
            })
        }
        composable("profile/{id}") { backStackEntry ->
            val id = backStackEntry.arguments?.getString("id")
            ProfileScreen(userId = id)
        }
    }
}

Passing arguments properly

composable(
    "profile/{id}",
    arguments = listOf(navArgument("id") { type = NavType.IntType })
) { entry ->
    val id = entry.arguments?.getInt("id") ?: 0
    ProfileScreen(id)
}

Going back

navController.popBackStack()           // go back one screen
navController.navigate("home") {
    popUpTo("home") { inclusive = true }  // clear the back stack
}

Keep navigation out of deep composables

Don’t pass the NavController deep into your UI. Instead, expose events (onOpenProfile: (Int) -> Unit) and handle navigation at the NavHost level. This keeps screens reusable and testable.

Bottom navigation

Combine a Scaffold with a NavigationBar for the common bottom-tab layout, each tab navigating to a destination while preserving its state.

Common mistakes

  • Passing the NavController everywhere instead of hoisting navigation events.
  • Forgetting to declare argument types, leading to crashes parsing them.
  • Not using popUpTo on login → home, leaving the login screen on the back stack.
Summary: Define screens in a NavHost, move with a NavController, declare typed arguments, and hoist navigation events instead of passing the controller down.