How to navigate in Jetpack Compose

Using Navigation Compose for type-safe routing, nested graphs, deep links, and bottom navigation

, updated

Navigation in Compose uses the navigation-compose library with a declarative NavHost pattern.

Add the dependency

dependencies {
    implementation("androidx.navigation:navigation-compose:2.8.0")
}

Basic navigation

// Define routes (type-safe with objects)
sealed class Screen(val route: String) {
    object Home : Screen("home")
    object Details : Screen("details/{itemId}") {
        fun createRoute(itemId: String) = "details/$itemId"
    }
    object Settings : Screen("settings")
}

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

    NavHost(navController = navController, startDestination = Screen.Home.route) {
        composable(Screen.Home.route) {
            HomeScreen(
                onNavigateToDetails = { itemId ->
                    navController.navigate(Screen.Details.createRoute(itemId))
                },
                onNavigateToSettings = {
                    navController.navigate(Screen.Settings.route)
                }
            )
        }
        composable(
            route = Screen.Details.route,
            arguments = listOf(navArgument("itemId") { type = NavType.StringType })
        ) { backStackEntry ->
            val itemId = backStackEntry.arguments?.getString("itemId") ?: ""
            DetailsScreen(
                itemId = itemId,
                onBack = { navController.popBackStack() }
            )
        }
        composable(Screen.Settings.route) {
            SettingsScreen(onBack = { navController.popBackStack() })
        }
    }
}

Type-safe navigation (Navigation 2.8+)

With Kotlin serialization, you get compile-time route safety:

// build.gradle.kts
plugins {
    id("org.jetbrains.kotlin.plugin.serialization") version "2.0.0"
}
dependencies {
    implementation("androidx.navigation:navigation-compose:2.8.0")
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0")
}

// Define serializable routes
@Serializable object HomeRoute
@Serializable data class DetailsRoute(val itemId: String)
@Serializable object SettingsRoute

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

    NavHost(navController = navController, startDestination = HomeRoute) {
        composable<HomeRoute> {
            HomeScreen(
                onNavigateToDetails = { id -> navController.navigate(DetailsRoute(id)) }
            )
        }
        composable<DetailsRoute> { backStackEntry ->
            val route = backStackEntry.toRoute<DetailsRoute>()
            DetailsScreen(itemId = route.itemId, onBack = { navController.popBackStack() })
        }
        composable<SettingsRoute> {
            SettingsScreen(onBack = { navController.popBackStack() })
        }
    }
}

Bottom navigation with Scaffold

@Composable
fun MainScreen() {
    val navController = rememberNavController()
    val currentBackStackEntry by navController.currentBackStackEntryAsState()
    val currentRoute = currentBackStackEntry?.destination?.route

    Scaffold(
        bottomBar = {
            NavigationBar {
                NavigationBarItem(
                    selected = currentRoute == "home",
                    onClick = { navController.navigate("home") { popUpTo("home") { inclusive = true } } },
                    icon = { Icon(Icons.Default.Home, contentDescription = "Home") },
                    label = { Text("Home") }
                )
                NavigationBarItem(
                    selected = currentRoute == "search",
                    onClick = { navController.navigate("search") },
                    icon = { Icon(Icons.Default.Search, contentDescription = "Search") },
                    label = { Text("Search") }
                )
                NavigationBarItem(
                    selected = currentRoute == "profile",
                    onClick = { navController.navigate("profile") },
                    icon = { Icon(Icons.Default.Person, contentDescription = "Profile") },
                    label = { Text("Profile") }
                )
            }
        }
    ) { innerPadding ->
        NavHost(
            navController = navController,
            startDestination = "home",
            modifier = Modifier.padding(innerPadding)
        ) {
            composable("home") { HomeScreen() }
            composable("search") { SearchScreen() }
            composable("profile") { ProfileScreen() }
        }
    }
}

Passing data between screens

// Type-safe with data class
@Serializable data class DetailsRoute(val itemId: String, val from: String? = null)

// Navigate
navController.navigate(DetailsRoute(itemId = "42", from = "home"))

// Nested graphs
navigation<SettingsGraph>(startDestination = SettingsOverview) {
    composable<SettingsOverview> { SettingsOverviewScreen() }
    composable<SettingsDetail> { SettingsDetailScreen() }
}
composable(
    route = "details/{itemId}",
    deepLinks = listOf(
        navDeepLink {
            uriPattern = "https://example.com/items/{itemId}"
            action = Intent.ACTION_VIEW
        }
    )
) { backStackEntry ->
    val itemId = backStackEntry.arguments?.getString("itemId") ?: ""
    DetailsScreen(itemId = itemId)
}

In your AndroidManifest.xml:

<activity android:name=".MainActivity">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="https" android:host="example.com" />
    </intent-filter>
</activity>