On this page
Navigation is where many Compose apps get messy: string routes with hand-parsed arguments, ViewModels that leak between screens, and back behaviour that surprises users. Jetpack Navigation 3, stable since late 2025, takes a simpler approach. Your back stack is plain Compose state, and screens are rendered from it.
For new Compose apps, Google now points to Navigation 3. Many existing apps use Navigation Compose (Navigation 2, with NavHost and NavController), which remains supported and gained type-safe routes. Both are covered below. Check the release notes for current API details, because Navigation 3 is still evolving.
The Navigation 3 mental model
Step 1: Define routes as keys
import androidx.navigation3.runtime.NavKey
import kotlinx.serialization.Serializable
@Serializable data object Home : NavKey
@Serializable data object ProductList : NavKey
@Serializable data class Product(val id: String) : NavKey // arguments are just properties
@Serializable data object Cart : NavKeyKeys are ordinary Kotlin types, so arguments are type-safe properties with no string parsing. Implementing NavKey and @Serializable lets Navigation 3 save the back stack across configuration changes and process death.
Step 2: Hold the back stack and render it
@Composable
fun AppNavigation() {
val backStack = rememberNavBackStack(Home) // saveable back stack
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
entryDecorators = listOf(
rememberSaveableStateHolderNavEntryDecorator(), // per-entry rememberSaveable state
rememberViewModelStoreNavEntryDecorator(), // per-entry ViewModels
),
entryProvider = entryProvider {
entry<Home> {
HomeScreen(onBrowse = { backStack.add(ProductList) })
}
entry<ProductList> {
ProductListScreen(onProductClick = { id -> backStack.add(Product(id)) })
}
entry<Product> { key ->
ProductScreen(
viewModel = viewModel { ProductViewModel(productId = key.id) },
onAddedToCart = { backStack.add(Cart) },
)
}
entry<Cart> {
CartScreen(onDone = { backStack.removeAll { it != Home } }) // pop back to Home
}
},
)
}The ViewModel decorator scopes a ViewModel to its back stack entry, so it's cleared when that entry is popped. Decorator function names and artifacts (for example lifecycle-viewmodel-navigation3) are listed in the Navigation 3 docs, so check them against your version.
Common navigation patterns
backStack.add(Product("42")) // navigate forward
backStack.removeLastOrNull() // back
backStack.removeAll { it is Product } // pop all product screens
backStack.clear(); backStack.add(Home) // reset (e.g. after logout)
// Avoid duplicates when tapping the same tab twice
fun NavBackStack<NavKey>.navigateTopLevel(key: NavKey) {
if (lastOrNull() != key) { removeAll { it != Home }; if (key != Home) add(key) }
}Because the back stack is a list you own, you can keep separate back stacks per bottom-navigation tab, or show two entries side by side on tablets using Navigation 3 scene strategies (for list-detail layouts).
Navigation Compose (Navigation 2) for existing apps
@Serializable object HomeRoute
@Serializable data class ProductRoute(val id: String)
@Composable
fun AppNavHost(navController: NavHostController = rememberNavController()) {
NavHost(navController, startDestination = HomeRoute) {
composable<HomeRoute> {
HomeScreen(onProductClick = { id -> navController.navigate(ProductRoute(id)) })
}
composable<ProductRoute> { entry ->
val route: ProductRoute = entry.toRoute()
ProductScreen(productId = route.id, onBack = navController::popBackStack)
}
}
}| Aspect | Navigation 3 | Navigation Compose (2.x) |
|---|---|---|
| Back stack | Your own list in Compose state | Owned by NavController |
| Routes | Any key type (NavKey for saving) | Serializable route classes (type-safe since 2.8) |
| Adaptive layouts | Scenes can show multiple entries | One destination at a time |
| Maturity in your codebase | Newer | Widely deployed |
Passing arguments to ViewModels
Pass only IDs through navigation, never whole objects, and let the destination's ViewModel load the data from a repository. This keeps the saved back stack small, avoids stale copies, and survives process death correctly.
Real-world structures
Bottom-nav app
One back stack per tab, preserved when switching tabs, with a top-level navigator that switches the active stack.
Auth flow
Show login keys until authenticated, then clear and push Home, so Back doesn't return to login.
Deep link into a product
Parse the URI into keys and build a synthetic back stack (Home, ProductList, Product) for natural Up and Back behaviour. See Deep Links and App Links.
Common mistakes
- Passing large objects as arguments instead of IDs.
- Navigating from inside composable bodies (during composition) instead of from event callbacks.
- Passing the back stack or NavController deep into components. Pass navigation lambdas instead.
- Forgetting ViewModel scoping, so ViewModels outlive their screens.
Frequently asked questions
Should I migrate from Navigation Compose to Navigation 3?
For new apps, start with Navigation 3. For existing apps, migrate when you need its flexibility (custom back stacks, adaptive scenes) and follow Google's migration guide incrementally.
How do I handle the system back gesture?
NavDisplay calls your onBack lambda. In Navigation 2, NavHost handles it automatically. Predictive back animations are supported in recent versions.
Can I still use Fragments?
Yes, with the Fragment-based Navigation component, but new Compose apps rarely need Fragments.
Conclusion
Navigation 3 makes navigation just state: a list of type-safe keys you modify, rendered by NavDisplay, with decorators for saved state and ViewModels. Navigation Compose remains a solid choice for existing apps. In both, pass IDs, navigate from events, and keep navigation out of reusable components.
Next step: render long lists efficiently with LazyColumn vs Column.
Official references
๐ฑ Apps built by Yunok Tech
Real Android apps on Google Play, free to download.
Walzi4K wallpapers for lock and home screenGet it โธ
NotiVaultNotification history: recover dismissed notificationsGet it โธ
Ready to build real Android applications?
Jetpack Compose clicks once you build real screens with real state. Practise modern Android UI development on projects that go beyond toy examples.
Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.
Explore the Android course and take your next step as an Android developer.