跪拜 Guibai
← All articles
Android · Android Jetpack · Architecture

Navigation Compose Ditches String Routes for Compile-Time Safety

By 杉氧 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

String-based routing has been a persistent source of runtime crashes and silent breakage in Android navigation. Compile-time type safety eliminates an entire class of bugs and makes refactoring safe, while the callback-hoisting pattern keeps Compose screens decoupled enough to test without a real navigation host.

Summary

Navigation in Compose moves from launching components to defining a graph of typed destinations. A sealed class hierarchy annotated with `@Serializable` replaces fragile string-based routes, so `navController.navigate(Screen.Profile(id))` carries no risk of typos or broken argument parsing. The `toRoute()` extension pulls parameters directly from the back stack entry without manual deserialization.

Architecturally, navigation logic stays out of UI composables through callback hoisting: screens expose lambdas like `onUserClick: (String) -> Unit`, and a top-level coordinator wires them to `navController` calls. This keeps composables previewable and testable in isolation. For cross-screen data, the guidance is to pass only minimal IDs — a `userId` rather than a full object — to avoid transaction size limits and let each screen's ViewModel fetch its own fresh state.

Pitfalls include never singletons a `NavController` outside composition context, using effect streams from ViewModels to trigger navigation, and splitting large graphs into nested sub-graphs for modular apps. Deep linking works directly on the type-safe destinations.

Takeaways
Destinations are defined as a sealed class with `@Serializable` objects and data classes, not string literals.
`NavHost` accepts the sealed class directly as a type parameter, and `toRoute()` deserializes parameters from the back stack entry.
UI composables should expose callback lambdas; navigation actions are wired in a single coordinator, not scattered inside screens.
Pass only minimal identifiers (e.g., `userId`) between screens to avoid `TransactionTooLargeException` and keep data fresh via ViewModels.
`NavController` must be created with `remember` inside composition; ViewModels should emit navigation events through an effect channel observed by the UI.
Type-safe destinations support deep links configured inside the `composable` block.
Large navigation graphs benefit from splitting into nested sub-graphs using the `navigation` DSL block.
Conclusions

Compile-time route checking is a genuine safety upgrade over string routes, but the real architectural win is the callback-hoisting pattern — it forces a clean separation that many teams skip when `navController` is too easy to pass around.

The advice to pass only IDs rather than objects is old wisdom, yet Compose's stateless recomposition model makes it more urgent: a recreated screen with stale parceled data looks correct but isn't.

Type-safe deep links close a gap that often pushed teams back toward manual intent parsing; having them declared in the same sealed class hierarchy keeps all entry points visible in one place.

Singleton `NavController` outside composition is a common mistake that survives code review because it appears to work until a configuration change or process death reveals the broken state binding.

Concepts & terms
Type-Safe Navigation
A Navigation Compose approach where destinations are Kotlin serializable classes rather than strings, giving compile-time verification of routes and parameters.
Callback Hoisting
A pattern where UI composables expose event lambdas instead of calling navigation directly; a parent coordinator wires those lambdas to `navController` calls, keeping screens decoupled.
NavHost
The Compose container that associates a `NavController` with a navigation graph and swaps composable content in and out as the current route changes.
toRoute()
A type-safe extension on `NavBackStackEntry` that deserializes the entry's arguments into the corresponding sealed class destination, avoiding manual bundle parsing.
From the discussion

The discussion pivots entirely to Navigation Compose 3 (Nav3), questioning the article's focus on Nav2. A defense of Nav2 rests on production stability and respect for legacy systems, arguing against reckless adoption of untested frameworks. A counterpoint champions Nav3's state-driven, list-based architecture as a more idiomatic Compose solution that offers finer control and serializable type safety.

An article on Nav2 in 2026 is outdated when Nav3 exists.
Stability and legacy project compatibility justify sticking with Nav2; new frameworks carry unknown risks that make blanket recommendations irresponsible.
Nav3's list-based, key-driven state management is a superior, more Compose-idiomatic paradigm that gives developers full control over the back stack and supports serializable types.
See top comments, translated →
Source: juejin.cn ↗ Google Translate ↗ Backup ↗