Navigation Compose Ditches String Routes for Compile-Time Safety
Foreword
In traditional Android development, page navigation is inseparable from Intent, FragmentManager, or Navigation XML. As a seasoned Android developer, you have definitely experienced the pain of handling onFragmentResult or been tortured by deeply nested Fragment stacks.
In the Compose era, pages are no longer Activities or Fragments but ordinary functions. So, how do we manage the switching of these functions? How do we handle parameter passing? And how do we ensure navigation logic doesn't pollute our UI architecture?
Today, we will tackle Navigation Compose and introduce modern Type-Safe Navigation.
1. Mindset Shift: From "Launching Components" to "Defining Destinations"
In Compose, the core of navigation is NavHost.
- Old mindset: I want to go to page A, so I launch Activity A.
- New mindset: I define a navigation graph (NavGraph), telling it what "Destinations" exist, and then change the "Route" to make
NavHostdisplay different destinations.
2. Modern Solution: Type-Safe Navigation (Kotlin Serialization)
Early Navigation Compose used string routes, which were very prone to typos, and passing parameters was as painful as concatenating URLs. The current best practice is to use Kotlin Serialization for full type safety.
1. Define Destinations
@Serializable
sealed class Screen {
@Serializable object Home : Screen()
@Serializable data class Profile(val userId: String) : Screen()
}
2. Build the Navigation Graph
val navController = rememberNavController()
NavHost(navController = navController, startDestination = Screen.Home) {
composable<Screen.Home> {
HomeScreen(onUserClick = { id ->
navController.navigate(Screen.Profile(id))
})
}
composable<Screen.Profile> { backStackEntry ->
val profile = backStackEntry.toRoute<Screen.Profile>()
ProfileScreen(userId = profile.userId)
}
}
Observe carefully: See? No string concatenation at all. The parameter userId is automatically parsed from the object, with compile-time checking.
3. Architecture Advancement: Decoupling Navigation Logic
As a senior architect, you absolutely don't want navController to seep into every UI component. This makes your UI difficult to preview and test in isolation.
Best Practice: Callback Hoisting
UI components are only responsible for exposing callbacks; navigation actions are handled uniformly by the outermost "navigation controller".
@Composable
fun UserList(onUserDetail: (String) -> Unit) {
// UI logic, only cares about clicks, not navigation
Button(onClick = { onUserDetail("123") }) { ... }
}
4. The "Deep Water" of Cross-Page Parameter Passing: Pass Object or Pass ID?
This is a perennial question. In the View system, we liked to pass Parcelable objects.
Recommendation for Compose Navigation: Only pass the minimal ID.
- Reason 1: The navigation stack has storage limits; passing large objects can cause
TransactionTooLargeException. - Reason 2: It aligns with MVI principles. Pass a
userIdto the target page's ViewModel, and let the ViewModel fetch the latest data from the database or network. This ensures data consistency after page navigation.
5. Navigation Pitfall Avoidance Guide for Developers
- Don't singleton NavController outside NavHost:
navControllermust be created viaremember, bound to the current composition context. If you want to navigate inside a ViewModel, define aNavigationEventstream (Effect) and let the UI layer listen and execute the navigation. - Handling Deep Links:
Type-safe navigation also supports Deep Links. You just need to configure them via parameters inside the
composableblock. - Nested Navigation:
For large apps, it's recommended to split the navigation graph into multiple sub-graphs (the
navigationblock in the Navigation DSL), which effectively manages modularized code.
Conclusion
The emergence of Navigation Compose allows Android development to truly realize a "Single Activity Architecture". Coupled with type-safe mechanisms, we finally bid farewell to the rigidity and uncertainty of the XML era.
Next up, we will face the practical issue every developer cares about most: Performance Optimization in Practice: How to Locate Redundant Recompositions and Squeeze Out Every Frame of Performance.
If you find this helpful, feel free to like and follow. We evolve in code and delve deep into principles.
Top 1 of 3 from juejin.cn, machine-translated. The original thread is authoritative.
Why write about nav2 in 2026 instead of nav3?
New technology is certainly good, but we also need to accommodate old projects. So, on the basis of seeking stability, I didn't write about nav3 released at the end of '25. First, I haven't used it in a project and don't know the pitfalls of the new framework; second, we must have reverence for legacy systems. Recklessly recommending everyone adopt a new tech framework isn't wise. I'll introduce nav3 on a small scale when I have time.
The philosophy of navigate3 is much better than 2, more Compose-like. It's directly state-driven UI, using a list to maintain the Compose interface. The list stores keys corresponding to screens, and the screen displays the UI for that key. A new screen means adding a new key to the list; going back removes the topmost key. This way, the key drives the UI update, and we can fully control the key list maintenance ourselves. Plus, the key can pass Kotlin serializable types. I think it's much better than 2 and worth a try.