跪拜 Guibai
← Back to the summary

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.


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.


5. Navigation Pitfall Avoidance Guide for Developers

  1. Don't singleton NavController outside NavHost: navController must be created via remember, bound to the current composition context. If you want to navigate inside a ViewModel, define a NavigationEvent stream (Effect) and let the UI layer listen and execute the navigation.
  2. Handling Deep Links: Type-safe navigation also supports Deep Links. You just need to configure them via parameters inside the composable block.
  3. Nested Navigation: For large apps, it's recommended to split the navigation graph into multiple sub-graphs (the navigation block 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.

Comments

Top 1 of 3 from juejin.cn, machine-translated. The original thread is authoritative.

终极负能量plus

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.

用户2730454269511  → 杉氧  · 1 likes

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.