Compose Navigation
Jetpack Compose Navigation gives you a typed graph of destinations, a back stack, deep links, and per-route ViewModels. The modern path (compose-navigation 2.8+) supports type-safe routes via @Serializable data classes, which removes the string-typed argument plumbing of older versions.
NavHost, typed routes, deep links, nested graphs
EXAMPLE
// build.gradle.kts
// implementation("androidx.navigation:navigation-compose:2.8.0")
// implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import kotlinx.serialization.Serializable
// 1) Typed destinations — describe routes as data classes
@Serializable object Catalog
@Serializable data class ProductDetail(val sku: String)
@Serializable data class Checkout(val orderId: String)
@Composable
fun ShopNav() {
val nav = rememberNavController()
NavHost(navController = nav, startDestination = Catalog) {
composable<Catalog> {
CatalogScreen(onProductClick = { sku ->
nav.navigate(ProductDetail(sku))
})
}
composable<ProductDetail> { entry ->
val args = entry.toRoute<ProductDetail>()
ProductDetailScreen(
sku = args.sku,
onBuy = { orderId -> nav.navigate(Checkout(orderId)) },
onBack = { nav.popBackStack() },
)
}
composable<Checkout>(
// 2) Deep link — open from a URL like shop://order/42
deepLinks = listOf(navDeepLink { uriPattern = "shop://order/{orderId}" })
) { entry ->
val args = entry.toRoute<Checkout>()
CheckoutScreen(args.orderId, onDone = {
nav.popBackStack(Catalog, inclusive = false)
})
}
}
}
// 3) Per-destination ViewModel scoped to the back-stack entry
@Composable
fun CheckoutScreen(orderId: String, onDone: () -> Unit) {
val vm: CheckoutViewModel = androidx.lifecycle.viewmodel.compose.viewModel()
val ui by vm.ui.collectAsState()
// .. UI uses ui state
}
class CheckoutViewModel : androidx.lifecycle.ViewModel() {
// tied to the entry's lifecycle; cleared when navigating away
val ui: kotlinx.coroutines.flow.StateFlow<Unit> = kotlinx.coroutines.flow.MutableStateFlow(Unit)
}
// 4) Nested graphs — group flows together
@Composable
fun RootNav() {
val nav = rememberNavController()
NavHost(nav, startDestination = "main") {
navigation(startDestination = Catalog, route = "main") {
composable<Catalog> { CatalogScreen() }
composable<ProductDetail> { ProductDetailScreen() }
}
navigation(startDestination = "login", route = "auth") {
composable("login") { LoginScreen() }
composable("signup") { SignupScreen() }
}
}
}
// 5) Pass results back without holding refs
// In Checkout:
// nav.previousBackStackEntry?.savedStateHandle?.set("orderResult", "ok")
// nav.popBackStack()
// In Catalog (after returning):
// val handle = nav.currentBackStackEntry?.savedStateHandle
// val result = handle?.getStateFlow<String?>("orderResult", null)?.collectAsState()
// (then read result.value)
// 6) Modal / dialog destinations
// composable<EditProfile>(arguments = ..., enterTransition = { ... }) { ... }
// or use NavDisplay (3rd party) for declarative side-sheets.
// 7) Backstack management
// nav.popBackStack(Catalog, inclusive = false)
// nav.navigate(Catalog) { popUpTo(Catalog) { inclusive = true } } // reset stack
// 8) Common pitfalls
// - Holding context-bound singletons in a ViewModel that lives across config changes
// - String-typed routes (older compose-nav) -> typos = runtime crashes; prefer typed routes
// - Long argument blobs in routes; use the SavedStateHandle or a per-flow VM repository
// Stubs to keep this snippet compileable
@Composable fun CatalogScreen(onProductClick: (String) -> Unit = {}) {}
@Composable fun ProductDetailScreen(sku: String = "", onBuy: (String) -> Unit = {}, onBack: () -> Unit = {}) {}
@Composable fun ProductDetailScreen() {}
@Composable fun LoginScreen() {}
@Composable fun SignupScreen() {}
Why it matters
Use the new typed routes (@Serializable data classes + composable
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
val nav = rememberNavController()
NavHost(nav, startDestination = "home") {
composable("home") { HomeScreen(nav) }
composable("profile/{id}") { back -> ProfileScreen(back.arguments?.getString("id")) }
}
Try it Yourself »
Discussion
Loading…