What you'll learn
Most apps aren't a single stack of screens — they have sections reached by bottom tabs, separate flows for sign-in versus the main app, and links that jump straight to a screen. This module covers the patterns that make multi-screen apps feel right.
By the end you'll be able to:
- Build bottom-tab navigation with
NavigationBar - Group related screens into nested graphs
- Use type-safe routes with data classes
- Understand deep links at a high level
Bottom navigation
For a few top-level sections, use a NavigationBar in the Scaffold's bottomBar slot. Each NavigationBarItem highlights when its route is current (read via currentBackStackEntryAsState()) and navigates on tap. The three navigation options shown are the standard recipe for tabs:
@Composable
fun MainScreen() {
val navController = rememberNavController()
val items = listOf("home", "search", "profile")
val backStackEntry by navController.currentBackStackEntryAsState()
val current = backStackEntry?.destination?.route
Scaffold(
bottomBar = {
NavigationBar {
items.forEach { route ->
NavigationBarItem(
selected = current == route,
onClick = {
navController.navigate(route) {
popUpTo(navController.graph.startDestinationId) { saveState = true }
launchSingleTop = true
restoreState = true
}
},
icon = { Icon(iconFor(route), contentDescription = route) },
label = { Text(route) },
)
}
}
},
) { padding ->
NavHost(navController, startDestination = "home", Modifier.padding(padding)) {
composable("home") { HomeScreen() }
composable("search") { SearchScreen() }
composable("profile") { ProfileScreen() }
}
}
}Tip
saveState / restoreState pair is what makes tabs feel native: switch away from a tab and back, and it remembers its scroll position and inner navigation instead of resetting.Nested graphs
As apps grow, a flat list of routes gets unwieldy. Group related screens into a nested graph with navigation(route = "...") — for example an auth graph (login, signup) separate from the main graph (feed, settings). You can navigate to the whole graph by its route, and it keeps each feature's screens tidily together:
NavHost(navController, startDestination = "auth") {
navigation(startDestination = "login", route = "auth") {
composable("login") { LoginScreen() }
composable("signup") { SignupScreen() }
}
navigation(startDestination = "feed", route = "main") {
composable("feed") { FeedScreen() }
composable("settings") { SettingsScreen() }
}
}Type-safe navigation
String routes work, but a typo like "detial/5" only fails at runtime, and you must hand-parse arguments. Modern Navigation Compose lets you use types as destinations: define a @Serializable object or data class per screen, and pass an actual instance. Arguments become real, type-checked properties — no string building, no manual parsing:
// Define destinations as types (Navigation 2.8+ with kotlinx.serialization):
@Serializable object Home
@Serializable data class Detail(val productId: Int)
NavHost(navController, startDestination = Home) {
composable<Home> {
HomeScreen(onOpen = { id -> navController.navigate(Detail(id)) })
}
composable<Detail> { entry ->
val detail: Detail = entry.toRoute()
ProductDetail(productId = detail.productId) // fully type-checked
}
}Key idea
Deep links (overview)
A deep link lets a URL or notification open a specific screen directly — tapping myapp://product/42 jumps straight to that product's detail page. You attach a deepLinks pattern to a composable destination and declare it in the manifest. You won't need it for early projects, but know it exists: it's how notifications, search results, and shared links land users on the right screen.
Recap & quick check
Key takeaways
- Use NavigationBar in the Scaffold's bottomBar for a few top-level sections.
- The tab recipe: popUpTo(startDestination){saveState}, launchSingleTop, restoreState — for native-feeling tabs.
- Group related screens into nested graphs with navigation(route = ...).
- Prefer type-safe navigation: @Serializable destinations give compile-checked routes and arguments.
- Deep links open a specific screen from a URL or notification.
Quick check
1. Which component provides bottom-tab navigation in Material 3?
2. What makes a tab remember its scroll position when you switch away and back?
3. Why group screens into nested graphs?
4. A key advantage of type-safe navigation is…
5. What is a deep link?
Your app's screens are wired together. But where should each screen's state live? Time to meet the ViewModel.