In late 2025, Google released Jetpack Navigation 3, a major redesign of the Android Navigation library built specifically for Jetpack Compose and modern UI architectures. This is not a typical incremental update. Navigation 3 fundamentally changes how navigation state is modeled, owned, and rendered in an application.
If you have been using the Navigation Component with Compose through navigation-compose, the new version may feel unfamiliar at first. However, it aligns much more naturally with Compose’s reactive programming model and solves many long-standing limitations around state ownership, adaptive layouts, and complex navigation flows.
This article explains why Navigation 3 matters, what has changed compared to Navigation 2, and how to approach migrating an existing codebase.
Why Navigation 3 Is a Big Deal
The biggest shift in Navigation 3 is philosophical rather than syntactical. Navigation is no longer driven by a hidden controller and a declarative graph. Instead, it is driven directly by application state that you own.
Navigation 3 is designed to be Compose-first. Instead of adapting a legacy API into Compose, the navigation model follows the same mental model as Compose itself: UI is a function of state. When the navigation state changes, the UI recomposes automatically. This removes a large amount of glue code, side effects, and lifecycle edge cases that existed in earlier versions.
Another major change is developer ownership of the back stack. Rather than delegating navigation history to an internal controller, your app manages a state list representing the active screens. This makes navigation predictable, testable, and easier to reason about. You can serialize it, inspect it, and manipulate it with standard Kotlin state operations. This approach also unlocks advanced patterns such as multi-pane layouts, simultaneous destinations on tablets, and custom back behaviors without fighting the framework.
Navigation 3 also improves support for modern UI expectations such as predictive back gestures, animated transitions, and adaptive layouts. Because navigation is just state, these behaviors integrate naturally with Compose animation APIs and window size classes.
Finally, Navigation 3 aligns with Compose Multiplatform. If you are building shared UI across Android, desktop, or iOS, the same navigation primitives can be reused, reducing platform-specific divergence and architectural complexity.
What Changed Compared to Navigation 2
In Navigation 2, the navigation controller owned the back stack internally and UI was rendered through a NavHost connected to a navigation graph. Even in Compose, the graph-driven model remained central, and many behaviors were implicit or difficult to customize.
Navigation 3 replaces this model entirely. The back stack is represented by a developer-managed state list. UI rendering happens through NavDisplay, which consumes that state and displays the appropriate composables. Navigation is no longer an opaque side effect; it is simply state mutation.
Compose integration is no longer an adapter layer but the primary design target. Adaptive layouts and multi-destination rendering become straightforward because multiple entries can be rendered simultaneously. The overall architecture becomes more modular and easier to test.
This is why Navigation 3 should be viewed as a new generation of the library rather than a simple upgrade.
How to Migrate an Existing Project
Migration is not a mechanical rename of APIs. It requires adopting the new state-driven mental model. The good news is that the migration can be incremental and does not require rewriting business logic or ViewModels.
Step 1: Add Navigation 3 Dependencies
Remove the old Navigation 2 dependencies and add the Navigation 3 runtime and UI artifacts. Always check the official release notes for the latest stable versions.
dependencies {
implementation "androidx.navigation3:navigation3-runtime:1.0.0"
implementation "androidx.navigation3:navigation3-ui:1.0.0"
}
Step 2: Define Navigation Keys and State
Instead of defining destinations in a navigation graph, define them as strongly typed keys. A sealed interface or sealed class works well.
@Serializable
sealed interface Screen : NavKey
object Home : Screen
object Details : Screen
Create a state holder for your back stack:
val backStack = remember { mutableStateListOf<Screen>(Home) }
This list becomes the single source of truth for navigation.
Step 3: Replace NavHost with NavDisplay
NavHost is replaced by NavDisplay. Rather than passing a graph, you pass the current navigation entries and an entry provider that maps keys to composables.
NavDisplay(
entries = backStack.toEntries(entryProvider),
onBack = { backStack.removeLast() }
)
The UI automatically reflects whatever is in the back stack.
Step 4: Update Navigation Actions
Navigation is no longer performed via NavController.navigate(). Instead, you mutate the back stack directly.
backStack.add(Details)
To go back:
backStack.removeLast()
This keeps navigation explicit, predictable, and easy to test.
Step 5: Remove Legacy Navigation APIs
Once all flows are migrated, remove old Navigation imports, plugins, and graph resources. Your app should now rely exclusively on the Navigation 3 APIs and your own state.
Practical Migration Advice
It is usually best to migrate one feature or flow at a time instead of converting the entire app in one large change. Isolate navigation logic into a small state holder or coordinator to keep composables clean and testable. Existing ViewModels, repositories, and business logic can remain unchanged.
If your app already uses Compose heavily, the migration tends to be straightforward. The biggest adjustment is mental: treating navigation as state rather than as a controller command.
Current Limitations and Future Direction
Navigation 3 is stable but still evolving. Some higher-level patterns such as deep linking, complex dialog flows, and advanced bottom navigation patterns are still maturing. Expect incremental improvements and additional tooling as adoption grows.
As with any foundational library, it is worth tracking release notes and official samples to stay aligned with recommended patterns.
Final Thoughts
Jetpack Navigation 3 represents a meaningful shift in how Android apps model navigation. By making navigation state explicit and Compose-native, it improves predictability, flexibility, and long-term maintainability. It also positions Android projects well for adaptive UI and multiplatform ambitions.
If your app is already Compose-first, adopting Navigation 3 is less about chasing new APIs and more about embracing a cleaner architectural model. The initial migration cost is real, but the clarity and control gained over navigation logic pays off quickly in larger codebases.
If you’d like, I can also help you turn this into a shorter version for Medium, Dev.to, or your internal engineering blog, or tailor examples to patterns you use in production Android apps.
Top comments (0)