Skip to content
intermediate Phase 4 · Jetpack Compose

Navigation

Navigate between screens with Navigation Compose, NavHost, and argument passing.

50m
3 problems
Topic Progress 0%

Argument Passing and Deep Links

Passing Arguments

Arguments are part of the route string or passed via navArgument:

// String argument
composable(
    route = "detail/{itemId}",
    arguments = listOf(
        navArgument("itemId") {
            type = NavType.StringType
            nullable = false
        }
    )
) { backStackEntry ->
    val itemId = backStackEntry.arguments?.getString("itemId")
    DetailScreen(itemId = itemId ?: "")
}

// Navigate with argument
navController.navigate("detail/product_123")

Optional Arguments with Default Values

composable(
    route = "search?query={query}",
    arguments = listOf(
        navArgument("query") {
            type = NavType.StringType
            defaultValue = ""
        }
    )
) { backStackEntry ->
    val query = backStackEntry.arguments?.getString("query") ?: ""
    SearchScreen(initialQuery = query)
}

// Navigate without optional arg (uses default)
navController.navigate("search")

// Navigate with optional arg
navController.navigate("search?query=kotlin")

Deep Links

Deep links let your app respond to URLs:

composable(
    route = "detail/{itemId}",
    arguments = listOf(navArgument("itemId") { type = NavType.StringType }),
    deepLinks = listOf(
        navDeepLink {
            uriPattern = "myapp://item/{itemId}"
        }
    )
) { backStackEntry ->
    val itemId = backStackEntry.arguments?.getString("itemId")
    DetailScreen(itemId = itemId ?: "")
}

Register the intent filter in AndroidManifest.xml:

<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="myapp" android:host="item" />
</intent-filter>

Back Stack Management

// Navigate (adds to back stack)
navController.navigate("detail/123")

// Pop back to a specific destination
navController.popBackStack("home", inclusive = false)

// Pop up to start destination (clears back stack)
navController.popBackStack(navController.graph.startDestinationId, inclusive = false)

// Navigate and clear back stack
navController.navigate("home") {
    popUpTo(navController.graph.startDestinationId) {
        inclusive = true
    }
    launchSingleTop = true
}

The popUpTo with inclusive = true clears the entire back stack. launchSingleTop prevents duplicate screens.

Nested Navigation and Navigation Patterns

Nested Navigation Graphs

For complex apps, group related screens into nested graphs:

NavHost(
    navController = navController,
    startDestination = "main"
) {
    // Main tab graph
    navigation(startDestination = Routes.HOME, route = "main") {
        composable(Routes.HOME) { HomeScreen() }
        composable(Routes.SEARCH) { SearchScreen() }
        composable(Routes.PROFILE) { ProfileScreen() }
    }

    // Detail flow
    composable(
        route = "detail/{itemId}",
        arguments = listOf(navArgument("itemId") { type = NavType.StringType })
    ) { backStackEntry ->
        val itemId = backStackEntry.arguments?.getString("itemId") ?: ""
        DetailScreen(
            itemId = itemId,
            onBack = { navController.popBackStack() }
        )
    }
}

Bottom Navigation with Navigation

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

    Scaffold(
        bottomBar = {
            NavigationBar {
                val navBackStackEntry by navController.currentBackStackEntryAsState()
                val currentRoute = navBackStackEntry?.destination?.route

                listOf(
                    "home" to Icons.Default.Home,
                    "search" to Icons.Default.Search,
                    "profile" to Icons.Default.Person
                ).forEach { (route, icon) ->
                    NavigationBarItem(
                        selected = currentRoute == route,
                        onClick = {
                            navController.navigate(route) {
                                popUpTo(navController.graph.findStartDestination().id) {
                                    saveState = true
                                }
                                launchSingleTop = true
                                restoreState = true
                            }
                        },
                        icon = { Icon(icon, contentDescription = route) },
                        label = { Text(route.replaceFirstChar { it.uppercase() }) }
                    )
                }
            }
        }
    ) { padding ->
        NavHost(
            navController = navController,
            startDestination = "home",
            modifier = Modifier.padding(padding)
        ) {
            composable("home") { HomeScreen() }
            composable("search") { SearchScreen() }
            composable("profile") { ProfileScreen() }
        }
    }
}

The key flags:

  • popUpTo(startDestination) with saveState = true -- avoids stacking duplicate screens
  • launchSingleTop = true -- prevents re-creating the same screen
  • restoreState = true -- restores tab state when switching back

Quiz

1. What does NavHost do in Compose navigation?

Question 1 options

2. How do you pass an argument to a route in Compose navigation?

Question 2 options

3. What does launchSingleTop do when navigating?

Question 3 options

4. What is the benefit of type-safe navigation with serializable routes?

Question 4 options

Flashcards

Question

What is the startDestination in NavHost?

Answer

The route that is displayed first when the NavHost is composed. It defines the initial screen of the navigation graph.

Question

How do you prevent duplicate screens in bottom navigation?

Answer

Use popUpTo(startDestination) with saveState=true, launchSingleTop=true, and restoreState=true when navigating between tabs.

Question

What is a deep link in Compose navigation?

Answer

A URI pattern that can open a specific screen directly, registered via navDeepLink in the composable() call and intent filters in the manifest.

Question

How do you read navigation arguments in the receiving composable?

Answer

From backStackEntry.arguments using getString(), getInt(), etc. With serializable routes, use backStackEntry.toRoute<T>().

Revision Notes

Key Takeaways

  • 1. NavHost renders the composable for the current route in the navigation graph
  • 2. Arguments are passed via route strings and read from backStackEntry.arguments
  • 3. Type-safe routes with serializable classes catch errors at compile time
  • 4. Bottom navigation needs popUpTo, launchSingleTop, and restoreState to work correctly
  • 5. Deep links allow your app to respond to external URLs

Interview Tips

  • Explain how bottom navigation prevents duplicate screen stacking
  • Discuss the difference between navigate() and popBackStack()
  • Know how to pass and read arguments in both string and serializable routes
  • Be ready to discuss deep link setup in both Compose and the manifest

Cheat Sheet

Navigation Cheat Sheet

Setup:

  • implementation("androidx.navigation:navigation-compose:2.7.6")
  • rememberNavController() + NavHost(navController, startDestination)

Routes:

  • String routes: composable("detail/{itemId}") { ... }
  • Serializable routes: composable<DetailRoute> { ... }

Arguments:

  • navArgument("name") { type = NavType.StringType }
  • Read via backStackEntry.arguments?.getString("name")

Navigation Actions:

  • navController.navigate("route") -- push
  • navController.popBackStack() -- go back
  • popUpTo + launchSingleTop + restoreState -- bottom nav

Deep Links:

  • navDeepLink { uriPattern = "myapp://path/{arg} }
  • Register intent filter in manifest