Files
vnidrop/.codex/skills/compose-skill/references/navigation-2.md
Hammed Abass fd4fff2c86 docs: add compose development guidelines and skills
Added a comprehensive collection of Compose development guidelines, best practices, and skill definitions in the .codex directory.

Co-authored-by: Junie <junie@jetbrains.com>
2026-07-07 16:55:43 +02:00

8.3 KiB

Navigation 2

NavHost, NavController, and graph DSL for Jetpack Compose navigation. Nav 2 is not deprecated and remains fully supported.

For shared navigation concepts (MVI rules, anti-patterns, version decision guide), see navigation.md. For DI wiring (Hilt/Koin + Nav 2), see navigation-2-di.md. For migrating to Nav 3, see navigation-migration.md.

References:

Core Concepts

Nav 2 has three building blocks:

  1. NavController — imperative controller that manages the back stack and navigation actions
  2. NavHost — composable container that maps routes to composable destinations
  3. NavGraph — the navigation graph defined via the NavHost DSL

Basic Setup with String Routes

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

    NavHost(navController = navController, startDestination = "home") {
        composable("home") {
            HomeScreen(onNavigateToDetail = { id -> navController.navigate("detail/$id") })
        }
        composable("detail/{itemId}") { backStackEntry ->
            val itemId = backStackEntry.arguments?.getString("itemId") ?: return@composable
            DetailScreen(itemId = itemId, onBack = { navController.navigateUp() })
        }
    }
}

How you wire ViewModels and state inside each composable block depends on your project's architecture — see navigation.md for the MVI boundary pattern where navigation is driven by ViewModel effects.

Type-Safe Routes (2.8+)

From Navigation Compose 2.8+, routes can be @Serializable types instead of strings. This is the recommended approach for new Nav 2 code:

@Serializable data object Home
@Serializable data class Detail(val itemId: String)

NavHost(navController = navController, startDestination = Home) {
    composable<Home> {
        HomeScreen(onNavigateToDetail = { id -> navController.navigate(Detail(id)) })
    }
    composable<Detail> { backStackEntry ->
        val detail: Detail = backStackEntry.toRoute()
        DetailScreen(itemId = detail.itemId, onBack = { navController.navigateUp() })
    }
}

Navigation Arguments (Legacy String Routes)

For pre-2.8 projects using string routes:

composable(
    route = "detail/{itemId}?sort={sort}",
    arguments = listOf(
        navArgument("itemId") { type = NavType.StringType },
        navArgument("sort") { type = NavType.StringType; defaultValue = "name" },
    )
) { backStackEntry ->
    val itemId = backStackEntry.arguments?.getString("itemId") ?: return@composable
    val sort = backStackEntry.arguments?.getString("sort") ?: "name"
    DetailScreen(itemId = itemId, sortBy = sort)
}

Type-safe routes (2.8+) are the recommended default — the navArgument DSL is for legacy codebases.

Common Navigation Actions

navController.navigate("detail/$id")

navController.navigate("detail/$id") {
    popUpTo("home") { inclusive = false }
    launchSingleTop = true
}

navController.navigateUp()

navController.popBackStack()

// Type-safe (2.8+)
navController.navigate(Detail(id)) {
    popUpTo<Home> { inclusive = false }
    launchSingleTop = true
}

Top-Level Tabs with NavigationBar

Use NavigationBar with currentBackStackEntryAsState(). Track selection with destination.hierarchy and hasRoute(route::class).

@Serializable sealed interface TopLevelRoute {
    @Serializable data object Home : TopLevelRoute
    @Serializable data object Search : TopLevelRoute
    @Serializable data object Profile : TopLevelRoute
}

@Composable
fun MainScreen() {
    val navController = rememberNavController()
    val navBackStackEntry by navController.currentBackStackEntryAsState()
    val currentDestination = navBackStackEntry?.destination
    val tabs = listOf(
        Triple(TopLevelRoute.Home, Icons.Default.Home, "Home"),
        Triple(TopLevelRoute.Search, Icons.Default.Search, "Search"),
        Triple(TopLevelRoute.Profile, Icons.Default.Person, "Profile"),
    )

    Scaffold(
        bottomBar = {
            NavigationBar {
                tabs.forEach { (route, icon, label) ->
                    val selected =
                        currentDestination?.hierarchy?.any { it.hasRoute(route::class) } == true
                    NavigationBarItem(
                        selected = selected,
                        onClick = {
                            navController.navigate(route) {
                                popUpTo(navController.graph.findStartDestination().id) {
                                    saveState = true
                                }
                                launchSingleTop = true
                                restoreState = true
                            }
                        },
                        icon = { Icon(icon, contentDescription = label) },
                        label = { Text(label) },
                    )
                }
            }
        },
    ) { padding ->
        NavHost(
            navController = navController,
            startDestination = TopLevelRoute.Home,
            modifier = Modifier.padding(padding),
        ) {
            composable<TopLevelRoute.Home> { HomeScreen(navController) }
            composable<TopLevelRoute.Search> { SearchScreen(navController) }
            composable<TopLevelRoute.Profile> { ProfileScreen(navController) }
        }
    }
}

Type-safe (2.8+):

composable<Detail>(
    deepLinks = listOf(
        navDeepLink<Detail>(basePath = "https://example.com/detail")
    )
) { backStackEntry ->
    val detail: Detail = backStackEntry.toRoute()
    DetailScreen(detail.itemId)
}

Navigate with Results

Pass data back via SavedStateHandle on back stack entries (avoids bloating route arguments):

// Sender: set on previous entry, then pop
Button(onClick = {
    navController.previousBackStackEntry?.savedStateHandle?.set("filter_result", selectedFilter)
    navController.navigateUp()
}) { Text("Apply") }

// Receiver: observe on current entry
val filterResult = navController.currentBackStackEntry
    ?.savedStateHandle
    ?.getStateFlow<String?>("filter_result", null)
    ?.collectAsStateWithLifecycle()

Nested Navigation Graphs

Group related destinations under a nested graph:

NavHost(navController = navController, startDestination = "home") {
    composable("home") { HomeScreen(navController) }

    navigation(startDestination = "checkout/cart", route = "checkout") {
        composable("checkout/cart") { CartScreen(navController) }
        composable("checkout/shipping") { ShippingScreen(navController) }
        composable("checkout/payment") { PaymentScreen(navController) }
    }
}

Type-safe: use navigation<Graph>(startDestination = Route) with @Serializable types — same structure as above.

Animations

Default transitions on NavHost:

NavHost(
    navController = navController,
    startDestination = Home,
    enterTransition = { slideInHorizontally(initialOffsetX = { it }) + fadeIn() },
    exitTransition = { slideOutHorizontally(targetOffsetX = { -it }) + fadeOut() },
    popEnterTransition = { slideInHorizontally(initialOffsetX = { -it }) + fadeIn() },
    popExitTransition = { slideOutHorizontally(targetOffsetX = { it }) + fadeOut() },
) { /* destinations */ }

Conditional Navigation (Auth Guards)

Redirect via startDestination and clear login from the stack after success:

@Composable
fun AppNavigation(isAuthenticated: Boolean) {
    val navController = rememberNavController()
    val startDestination = if (isAuthenticated) Home else Login

    NavHost(navController = navController, startDestination = startDestination) {
        composable<Login> {
            LoginScreen(onLoginSuccess = {
                navController.navigate(Home) {
                    popUpTo<Login> { inclusive = true }
                }
            })
        }
        composable<Home> { HomeScreen(navController) }
        composable<Detail> { DetailScreen(navController) }
    }
}