Now in Android 的 `:core:navigation` 模块源码解析:基于 Navigation 3 的多栈导航状态机

发布时间:2026/9/13 11:49:40
Now in Android 的 `:core:navigation` 模块源码解析:基于 Navigation 3 的多栈导航状态机
Now in Android 的:core:navigation模块源码解析基于 Navigation 3 的多栈导航状态机【免费下载链接】nowinandroidA fully functional Android app built entirely with Kotlin and Jetpack Compose项目地址: https://gitcode.com/GitHub_Trending/no/nowinandroid本篇技术指南以 Now in AndroidNIA示例应用中core/navigation/README.md模块文档为骨架深入其源码实现讲解该应用如何基于 AndroidX Navigation 3 构建「顶层多返回栈 子栈」的导航状态模型。读完本文你将掌握NavigationState状态容器的设计、Navigator对导航事件的统一处理去重、单顶、返回、rememberNavigationState的配置持久化实现以及它们如何在NiaApp中与NavDisplay衔接驱动整个应用的页面流转。模块定位:core:navigation在模块化架构中的角色:core:navigation是 NIA 应用core层下的一个 Android 库模块。模块文档core/navigation/README.md用 Mermaid 依赖图明确标注了它的模块类型navigation[navigation]:::android-library即这是一个纯 Android 库与core层其他模块如model、data、designsystem并列且不直接依赖任何 feature 模块。该模块只包含两个核心类文件与一组测试Navigator.kt—— 导航事件处理器前进/返回NavigationState.kt—— 导航状态容器、Composable 工厂与状态转NavEntry的桥接NavigatorTest.kt—— 对导航行为的单元测试。从build.gradle.ktscore/navigation/build.gradle.kts可以看出其依赖策略api(libs.androidx.navigation3.runtime)将 Navigation 3 运行时 API 暴露给下游app 层直接引用该模块的类而lifecycle-viewmodel-navigation3、savedstate-compose作为内部实现细节使用implementation隐藏。这意味着整个应用对 Navigation 3 的依赖都收敛在:core:navigation模块内其余模块只需面向Navigator/NavigationState编程这是模块化架构中隔离框架依赖的典型手法。双栈模型一个顶层栈 每顶层一份子栈理解该模块的关键在于其状态结构。NavigationStateNavigationState.kt维护了两层返回栈class NavigationState( val startKey: NavKey, // 起始键用户从该键退出应用 val topLevelStack: NavBackStackNavKey, // 顶层栈只容纳顶层键 val subStacks: MapNavKey, NavBackStackNavKey, // 每个顶层键各有一份子栈 ) { val currentTopLevelKey: NavKey by derivedStateOf { topLevelStack.last() } val topLevelKeys get() subStacks.keys val currentSubStack: NavBackStackNavKey get() subStacks[currentTopLevelKey] ?: error(Sub stack for $currentTopLevelKey does not exist) val currentKey: NavKey by derivedStateOf { currentSubStack.last() } }设计要点topLevelStack记录用户访问过的顶层目的地顺序last()即当前顶层返回时从尾部移除即可回到上一个顶层栈。subStacks为每个顶层键维护独立子栈实现「每个 Tab 保留各自页面历史」的多栈行为类似底部导航的典型需求切换 Tab 不丢失该 Tab 内的详情页栈。currentKey与currentTopLevelKey均通过derivedStateOf派生保证 Compose 只在相关栈尾变化时重组。访问currentSubStack时若键不存在会抛出明确错误帮助在开发期快速暴露状态不一致。NavKey是 Navigation 3 中的目的地标识。NIA 中每个 feature 模块在api子模块里声明自己的键例如ForYouNavKey.kt中的Serializable object ForYouNavKey : NavKey、SearchNavKey.kt中的SearchNavKey而带参数的键如TopicNavKey含id则以Serializable data class形式携带参数。所有键通过Serializable注解获得序列化能力供状态保存/恢复使用。状态工厂rememberNavigationState与配置变更/进程死亡恢复rememberNavigationStateNavigationState.kt是创建状态的 Composable 入口Composable fun rememberNavigationState( startKey: NavKey, topLevelKeys: SetNavKey, ): NavigationState { val topLevelStack rememberNavBackStack(startKey) val subStacks topLevelKeys.associateWith { key - rememberNavBackStack(key) } return remember(startKey, topLevelKeys) { NavigationState(startKey, topLevelStack, subStacks) } }rememberNavBackStack(startKey)是 Navigation 3 提供的可记忆返回栈底层通过rememberSaveable持久化从而在配置变更旋转屏幕与进程死亡后自动恢复栈内容这与该函数的 KDoc「persists config changes and process death」完全对应。subStacks为每个顶层键各创建一个独立可保存的NavBackStackremember(startKey, topLevelKeys)保证当起始键或顶层键集合变化时才重建状态容器。在应用侧NiaAppState.kt中这样初始化val navigationState rememberNavigationState(ForYouNavKey, TOP_LEVEL_NAV_ITEMS.keys)即以「For You」页为起始键以TopLevelNavItem.kt中定义的TOP_LEVEL_NAV_ITEMS键集合ForYou、Bookmarks、Interests为顶层集合。事件处理器Navigator的前进与返回语义NavigatorNavigator.kt将所有导航事件收敛为对NavigationState的栈操作屏蔽了栈细节。其navigate(key)采用三分支策略fun navigate(key: NavKey) { when (key) { state.currentTopLevelKey - clearSubStack() // 点击当前 Tab清空其子栈 in state.topLevelKeys - goToTopLevel(key) // 切换到其他顶层 Tab else - goToKey(key) // 普通目的地压入当前子栈 } }三个分支对应三种语义重复点击当前顶层目的地→clearSubStack()保留子栈根部键清空其余条目subList(1, size).clear()回到该 Tab 的根页面这是标准的「点击已选 Tab 回首页」行为。切换到另一个顶层目的地→goToTopLevel(key)若目标是startKey则clear()后只保留它确保从其他 Tab 回起始页时栈干净否则remove(key)后add(key)实现去重 移到末尾的单顶语义。普通非顶层目的地→goToKey(key)同样「先移除再追加」保证同一目的地不会在子栈中重复出现singleTop 行为并将它变为栈顶。goBack()则根据当前位置决定回退到哪一层fun goBack() { when (state.currentKey) { state.startKey - error(You cannot go back from the start route) state.currentTopLevelKey - state.topLevelStack.removeLastOrNull() // 子栈栈底退回上一顶层栈 else - state.currentSubStack.removeLastOrNull() // 普通页面弹栈 } }若当前正处于起始键直接抛错防止把应用退到空栈throwOnEmptyBackStack测试验证了这一点若当前是该顶层子栈的根部则说明用户要离开该 Tab此时从topLevelStack移除末尾回到上一个顶层栈否则仅弹出当前子栈的栈顶页面。注意removeLastOrNull()返回null时被忽略——栈由rememberNavBackStack保证至少含起始根键因此不会出现空栈操作异常但这与「起始键处抛错」共同构成了对栈不变量的双重防护。状态到界面的桥接toEntries与 Entry DecoratorNavigationState无法直接被 UI 消费需要通过toEntriesNavigationState.kt转换为NavEntry列表Composable fun NavigationState.toEntries( entryProvider: (NavKey) - NavEntryNavKey, ): SnapshotStateListNavEntryNavKey { val decoratedEntries subStacks.mapValues { (_, stack) - val decorators listOf( rememberSaveableStateHolderNavEntryDecoratorNavKey(), rememberViewModelStoreNavEntryDecoratorNavKey(), ) rememberDecoratedNavEntries(stack, decorators, entryProvider) } return topLevelStack.flatMap { decoratedEntries[it] ?: emptyList() }.toMutableStateList() }实现细节对每个子栈通过rememberDecoratedNavEntries创建带装饰器的条目列表两个装饰器分工明确rememberSaveableStateHolderNavEntryDecorator负责页面内rememberSaveable状态在导航离开/返回时的保存与恢复rememberViewModelStoreNavEntryDecorator保证每个导航条目的ViewModelStore生命周期正确返回销毁、前进重建最终按topLevelStack的顺序把各子栈条目扁平化返回 Compose 可观察的SnapshotStateList——当任一栈内容变化时UI 自动重组。应用集成从NiaAppState到NavDisplay在 app 层导航状态被封装进NiaAppStateNiaAppState.kt并通过NavigationTrackingSideEffect将当前currentKey写入 JankStats 的指标状态用于导航帧率统计。UI 侧NiaApp.kt的接线方式val navigator remember { Navigator(appState.navigationState) } NiaNavigationSuiteScaffold( navigationSuiteItems { TOP_LEVEL_NAV_ITEMS.forEach { (navKey, navItem) - item( selected navKey appState.navigationState.currentTopLevelKey, onClick { navigator.navigate(navKey) }, // 导航栏点击 → navigate ... ) } }, ) { val entryProvider entryProvider { forYouEntry(navigator) bookmarksEntry(navigator) interestsEntry(navigator) topicEntry(navigator) searchEntry(navigator) } NavDisplay( entries appState.navigationState.toEntries(entryProvider), sceneStrategy listDetailStrategy, onBack { navigator.goBack() }, // 系统返回 → goBack ) }Navigator用remember缓存与应用状态同生命周期底部导航栏每个 item 的onClick统一走navigator.navigate(navKey)选中态由currentTopLevelKey驱动各 feature 模块以扩展函数注册条目例如forYouEntry将ForYouScreen(onTopicClick navigator::navigateToTopic)绑定到ForYouNavKey带参数的topicEntry通过metadata ListDetailSceneStrategy.detailPane()声明其为「详情面板」目的地配合rememberListDetailSceneStrategy实现大屏下的 List-Detail 自适应布局其 ViewModel 以key id的方式从 Hilt 工厂创建NavDisplay的onBack与系统返回手势绑定统一交给navigator.goBack()页面内深链式跳转如点击新闻跳转 Topic通过扩展函数navigator::navigateToTopic注入各屏幕避免 feature 直接依赖其他 feature 的实现细节。NiaAppState还利用currentTopLevelKey判断是否展示渐变背景仅 For You 页并在topLevelNavKeysWithUnreadResources中结合数据仓库为导航项计算未读红点notificationDot展示状态派生与数据层在此处的协同。行为验证NavigatorTest覆盖的导航语义模块自带的测试NavigatorTest.kt用三个顶层键与两个普通键验证了全部核心语义是理解模块行为的可执行文档测试方法验证的导航语义testStartKey初始状态位于起始顶层键testNavigate普通键导航进入当前子栈testNavigateTopLevel切换顶层目的地testNavigateSingleTop同一普通键重复导航不重复入栈singleToptestNavigateTopLevelSingleTop返回当前 Tab 时清空其子栈至根部testSubStack/testMultiStack多顶层各自独立维护子栈来回切换保留各自栈testPopOneNonTopLevel单次返回弹出子栈顶部testPopOneTopLevel子栈根部返回时退回上一个顶层栈popMultipleNonTopLevel/popMultipleTopLevel连续多次返回的累计弹栈行为throwOnEmptyBackStack起始键处返回抛出IllegalStateException这些测试全部基于纯 JVM 的NavBackStack构造NavigationState不依赖 Android 环境因此运行快、可读性强。例如testNavigateSingleTop断言navigate(TestKeyFirst)两次后子栈仍为[FirstTopLevel, TestKeyFirst]精确对应goToKey的「先 remove 再 add」实现。小结与延伸阅读:core:navigation模块以约百行代码实现了完整、可测试、可持久化的多栈导航模型NavigationState承载「顶层栈 每顶层子栈」的结构化状态Navigator以三分支navigate与分层goBack收敛所有导航事件toEntries借助 Navigation 3 的装饰器机制把状态映射为可组合 UI 条目。它在模块化上隔离了框架依赖在行为上用单元测试锁定了语义——这也是 NIA 作为官方架构示例所倡导的「状态驱动导航」模式。建议继续阅读导航状态与事件NavigationState.kt、Navigator.kt导航行为测试NavigatorTest.kt应用层集成NiaApp.kt、NiaAppState.kt、TopLevelNavItem.ktfeature 侧导航键与条目注册示例ForYouNavKey.kt、ForYouEntryProvider.kt、TopicEntryProvider.kt模块构建配置core/navigation/build.gradle.kts【免费下载链接】nowinandroidA fully functional Android app built entirely with Kotlin and Jetpack Compose项目地址: https://gitcode.com/GitHub_Trending/no/nowinandroid创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考