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

Migrating 170 Screens to Android Navigation 3: Overtime, Crashes, and Bottom-Sheet Surprises

By 稀有猿诉 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Navigation 3 is Google’s forward path for Compose-first, adaptive Android apps, but migrating a large production app is a one-shot, high-risk operation with no incremental escape hatch. The serialization contract is strict and easy to break in ways that only surface in production, and bottom sheets demand explicit lifecycle management that the framework doesn’t enforce.

Summary

A single developer rewrote an entire 170-screen Android app’s navigation layer from Navigation 2 to Navigation 3 over two weeks of overtime, while the rest of the team kept shipping features on the old stack. The migration was all-or-nothing: incremental adoption is impossible because Nav2 and Nav3 share no backward compatibility. Bottom sheets became the biggest unplanned headache—Nav3 ships no built-in bottom-sheet support, forcing a custom metadata wrapper around entries that later got replaced by Google’s SceneStrategy API.

The first production rollout to 1% of users immediately triggered `SerializationException` crashes. Passing a non-serializable `NavKey` as a navigation argument broke at runtime; the fix was a thin `@Serializable` wrapper class. A month and a half later, Google released a Claude Code skill purpose-built for Nav3 migrations. Retroactively testing it on the pre-migration branch showed it would have cut the work roughly in half, though legacy extensions and custom navigation logic still required manual intervention.

Deep links got a quiet overhaul during the migration—centralizing previously scattered logic into a single place with type-safe, serializable route definitions. The payoff is faster debugging and simpler onboarding for new screens. The team now treats adaptive layouts and multi-pane UIs as a configuration detail rather than a separate project.

Takeaways
Navigation 2 and Navigation 3 have zero backward compatibility; every screen must be migrated in a single pass.
Bottom sheets have no first-class Nav3 API out of the box—early adopters built custom metadata wrappers, later replaced by Google’s SceneStrategy.
Passing a non-serializable `NavKey` as a navigation argument causes a `kotlinx.serialization.SerializationException` crash at runtime.
Navigation-driven bottom sheets stay on the back stack unless manually dismissed, so navigating forward from one and pressing back re-shows the sheet.
Google’s post-migration Claude Code skill for Nav3 would have saved roughly one week of the two-week manual effort.
Deep-link logic scattered across composable declarations was centralized into a single, type-safe, serializable structure during the migration.
Merge conflicts from parallel feature development on the old navigation stack forced a deliberate crunch to finish fast.
Automated regression tests caught issues that were nearly impossible to reproduce manually before the 1% rollout.
Adaptive layouts and multi-pane UIs become a simple `sceneStrategy` configuration after migration rather than a separate engineering project.
Fragment-based apps must eliminate Fragments entirely before considering Nav3.
Conclusions

Nav3’s all-or-nothing migration model creates a perverse incentive to rush: the longer you take, the more merge debt you accumulate against a moving codebase.

Bottom sheets are the sharpest edge in Nav3—no framework guardrails, no compile-time safety, and a runtime behavior that silently breaks the back stack if you forget one dismiss call.

Serialization is the new runtime contract for navigation arguments, and it fails silently until a user hits the exact flow that passes a non-serializable key.

Google’s own AI tooling for Nav3 migration arrived too late for early adopters, which means the first wave of teams paid a tax that later teams can largely skip.

Deep links are often an afterthought in navigation design, but migrating them into a centralized, type-safe structure turned out to be one of the highest-ROI changes.

Automated testing surfaced navigation bugs that manual QA would have missed entirely, reinforcing that navigation state is just another state machine that needs deterministic verification.

The migration effectively deleted a large chunk of legacy navigation extension functions—code that existed only to patch gaps in Nav2’s API surface.

Concepts & terms
Navigation 3 (Nav3)
Android’s next-generation Jetpack navigation library that treats the navigation stack as typed, serializable state, replacing the string-based route system of Navigation 2.
SceneStrategy
A Nav3 API that lets developers define how a destination is displayed—e.g., as a full screen, a bottom sheet, or a side pane—without custom wrapper code.
NavKey
A type-safe, serializable identifier for a navigation destination in Nav3, replacing the string routes used in Navigation 2.
ModalBottomSheetLayout
A Material 3 Compose layout that displays a bottom sheet overlay; used as a workaround to render bottom-sheet destinations before SceneStrategy was available.
MVI (Model-View-Intent)
A unidirectional data-flow architecture pattern where the UI state is a single source of truth, making Nav3’s state-driven navigation stack a natural fit.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗