跪拜 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 ↗