๐ŸŽจ Jetpack Compose

Navigation in Jetpack Compose

Navigation 3 rethinks navigation for Compose: the back stack is just a list in Compose state that you own and modify. This guide shows how it works, how to pass arguments and scope ViewModels, and how it compares with the Navigation Compose (Navigation 2) API many apps still use.

โœ๏ธ Yunok Tech ๐Ÿ“… Updated โฑ 3 min read ๐ŸŽฏ Intermediate
On this page
  1. The Navigation 3 mental model
  2. Step 1: Define routes as keys
  3. Step 2: Hold the back stack and render it
  4. Common navigation patterns
  5. Navigation Compose (Navigation 2) for existing apps
  6. Passing arguments to ViewModels
  7. Real-world structures
  8. Common mistakes
  9. Frequently asked questions
  10. Conclusion

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.

Which API should you use?

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

Navigation 3: back stack as state rendered by NavDisplay The back stack is a list of keys: Home, ProductList, Product with id 42. NavDisplay reads the list and shows the entry for the last key, using an entry provider that maps each key type to a composable. Adding a key navigates forward; removing the last key navigates back. backStack (state) Home ProductList Product(id = "42") โ† top NavDisplayentryProvider maps keys ProductScreenid = "42" add() = navigate ยท removeLastOrNull() = back
No hidden controller state: what you see in the list is what's on screen.

Step 1: Define routes as keys

Routes.ktKotlin
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 : NavKey

Keys 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

AppNavigation.ktKotlin
@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

Back stack operationsKotlin
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).

Type-safe routes in Navigation ComposeKotlin
@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)
        }
    }
}
AspectNavigation 3Navigation Compose (2.x)
Back stackYour own list in Compose stateOwned by NavController
RoutesAny key type (NavKey for saving)Serializable route classes (type-safe since 2.8)
Adaptive layoutsScenes can show multiple entriesOne destination at a time
Maturity in your codebaseNewerWidely 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.

See all our apps โ†’

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.

Share: LinkedIn X / Twitter WhatsApp