Navigation 3 for Compose Multiplatform: Should You Migrate?

I recently finished a Compose Multiplatform project using Navigation 2, and I kept running into the same fundamental issue: Navigation 2 wasn’t built with Compose in mind. The imperative navigation calls (navController.navigate()) felt awkward in a declarative UI. The backstack was a black box I couldn’t easily inspect or reason about. The patterns that worked well in traditional Android development created friction in the Compose world.
So when Navigation 3 was announced—built from the ground up for Compose, treating navigation as state rather than imperative commands—I had to dig in. Here’s what looks promising, what concerns me, and how I’m thinking about migration timing for future projects.
What is Navigation 3?
Navigation 3 is a rewritten navigation library designed specifically for Jetpack Compose and Compose Multiplatform. Unlike its predecessor, it doesn’t rely on a hidden internal backstack managed by a NavController. Instead, navigation in Navigation 3 is simply state management.
The core philosophy is: Your backstack is just a list of data. When you update that list, the UI updates automatically to reflect the new state. This makes navigation feel like any other part of your Compose app.
Key Differences: Navigation 2 vs Navigation 3
| Feature | Navigation 2 | Navigation 3 |
|---|---|---|
| Backstack Ownership | Internal/Hidden (managed by library) | Developer-owned (managed as state) |
| Routing | String-based or Safe Args (often brittle) | Purely Type-Safe (Serializable classes) |
| UI Component | NavHost | NavDisplay |
| Logic Location | Imperative calls (e.g., controller.navigate) | Declarative state changes (list manipulation) |
| Adaptive Support | Limited (one destination at a time) | Native support for multi-pane layouts (e.g., List-Detail) |
Should You Migrate Now?
Based on my Navigation 2 experience and Navigation 3 research, here’s my current thinking on migration timing:
Consider Nav3 for new projects if:
- You’re starting fresh with no Navigation 2 code to port
- You need adaptive layouts or multi-pane navigation (Nav3’s native support is a significant win)
- You want type-safe routing without the brittleness of string-based navigation
- You value treating navigation as state rather than imperative controller calls
Stick with Nav2 for existing projects if:
- You have a working Navigation 2 implementation in production that meets your needs
- Your team is under tight deadlines and can’t afford refactoring effort mid-sprint
- The migration cost (time, testing, retraining) outweighs the developer experience benefits
- Your navigation requirements are simple and Nav2’s limitations don’t affect you
My honest take:
Navigation 3’s philosophy is compelling—treating navigation as state feels right for Compose. The alpha label on the CMP port shouldn’t scare you away—this is essentially Google’s stable Android Navigation 3 API adapted for multiplatform. The core patterns are proven.
For existing projects, migration comes down to ROI: is the improved developer experience worth the refactoring effort? For new projects, I’d start with Nav3 without hesitation—the type-safety and state-based approach are significant improvements over Nav2.
Navigation 3 in KMP vs. Android-Only Projects
While the core APIs are identical, using Navigation 3 in a Kotlin Multiplatform project requires a few specific adjustments:
- Polymorphic Serialization: For non-JVM platforms like iOS and Web, you must implement polymorphic serialization for your destination keys to ensure they can be saved and restored correctly across process deaths.
- Platform-Specific Handlers: In KMP, you’ll use
NavigationBackHandler(replacing the deprecatedPredictiveBackHandler) to handle native gestures like “swipe to back” on iOS or the physical back button on Android. - Adaptive Strategies: KMP projects often target desktop or web. Nav3 allows you to pass a
SceneStrategy(likeListDetailSceneStrategy) toNavDisplayto automatically switch between mobile and tablet/desktop layouts based on window size.
Code Sample: Implementing Navigation 3 in commonMain
The following implementation is adapted from official documentation and sample projects. I haven’t battle-tested this in production yet, so treat it as a starting point for exploration rather than production-ready architecture. That said, the patterns shown here align with Nav3’s design philosophy and should give you a solid foundation.
To integrate Navigation 3 into your Compose Multiplatform (CMP) project, you must use specific artifacts maintained by JetBrains that mirror the official Android Jetpack releases.
1. Update Version Catalog (libs.versions.toml)
[versions]
# Check for latest 2026 releases in the JetBrains Maven repository
jetbrains-navigation3 = "1.0.0-alpha05"
jetbrains-lifecycle = "2.10.0-alpha05"
jetbrains-adaptive = "1.3.0-alpha02"
[libraries]
navigation3-ui = { module = "org.jetbrains.androidx.navigation3:navigation3-ui", version.ref = "jetbrains-navigation3" }
# Add if using ViewModels with Navigation
navigation3-lifecycle = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "jetbrains-lifecycle" }
# Add for Material 3 Adaptive Layouts
navigation3-adaptive = { module = "org.jetbrains.compose.material3.adaptive:adaptive-navigation3", version.ref = "jetbrains-adaptive" }
2. Configure build.gradle.kts
Add the dependencies to your commonMain source set. Navigation 3 relies heavily on Kotlin Serialization for type-safe routing, so ensure the serialization plugin is enabled.
plugins {
kotlin("multiplatform")
alias(libs.plugins.jetbrainsCompose)
alias(libs.plugins.kotlin.serialization) // Required for type-safe routes
}
kotlin {
sourceSets {
commonMain.dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(libs.navigation3.ui)
// Optional: for shared ViewModels in NavDisplay
implementation(libs.navigation3.lifecycle)
}
}
}
3. Define Your Routes
Routes must implement the NavKey marker interface and be @Serializable to work across platforms.
import kotlinx.serialization.Serializable
@Serializable
sealed interface Screen : NavKey {
@Serializable data object Home : Screen
@Serializable data class Details(val itemId: String) : Screen
}
4. Manage State in a ViewModel
Because the backstack is just a list, you can manage it in your shared ViewModel.
class NavViewModel : ViewModel() {
// Current backstack is a snapshot list of Screen objects
var backStack by mutableStateOf<List<Screen>>(listOf(Screen.Home))
private set
fun navigateTo(screen: Screen) {
backStack = backStack + screen
}
fun pop() {
if (backStack.size > 1) backStack = backStack.dropLast(1)
}
}
5. Set Up NavDisplay in Your Composable
Finally, use NavDisplay to render your navigation based on the current backstack.
@Composable
fun AppNavigation(viewModel: NavViewModel = viewModel()) {
NavDisplay(
backStack = viewModel.backStack,
onBack = { viewModel.pop() },
entryProvider = entryProvider {
// Map serializable routes to Composables
entry<Screen.Home> {
HomeScreen(onDetailClick = { id -> viewModel.navigateTo(Screen.Details(id)) })
}
entry<Screen.Details> { entry ->
val details = entry.toRoute<Screen.Details>()
DetailScreen(itemId = details.itemId)
}
},
// Handles animations and adaptive layouts automatically
transitionSpec = { fadeIn() togetherWith fadeOut() }
)
}
Summary
Navigation 3 represents a genuine philosophical shift—from imperative navigation calls to declarative state management. The promise is compelling, especially for those of us who’ve wrestled with Navigation 2’s limitations in Compose applications.
The CMP port may be labeled alpha, but don’t let that discourage you. The underlying API comes from Google’s stable Android implementation, which means the patterns and APIs are solid. The alpha label reflects the CMP adaptation process, not fundamental API instability.
For new CMP projects, I’d use Nav3 without reservation. The type-safety, state-based navigation, and native adaptive layout support are significant improvements. For existing Nav2 projects, it’s a question of priorities—the refactoring effort versus the developer experience gains.
Evaluating navigation strategies for your Compose Multiplatform project?
Whether you’re considering Nav3, optimizing Nav2, or exploring alternatives, we’d love to talk through the tradeoffs. Let’s discuss your navigation architecture.
For more on our Kotlin Multiplatform work, check out our posts on KMP and CMP and CMP practical examples.