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 Android(NIA)示例应用中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.kts(core/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编程,这是模块化架构中隔离框架依赖的典型手法。
双栈模型:一个顶层栈 + 每顶层一份子栈
理解该模块的关键在于其状态结构。NavigationState(NavigationState.kt)维护了两层返回栈:
class NavigationState( val startKey: NavKey, // 起始键,用户从该键退出应用 val topLevelStack: NavBackStack<NavKey>, // 顶层栈:只容纳顶层键 val subStacks: Map<NavKey, NavBackStack<NavKey>>, // 每个顶层键各有一份子栈 ) { val currentTopLevelKey: NavKey by derivedStateOf { topLevelStack.last() } val topLevelKeys get() = subStacks.keys val currentSubStack: NavBackStack<NavKey> 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与配置变更/进程死亡恢复
rememberNavigationState(NavigationState.kt)是创建状态的 Composable 入口:
@Composable fun rememberNavigationState( startKey: NavKey, topLevelKeys: Set<NavKey>, ): 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为每个顶层键各创建一个独立可保存的NavBackStack;remember(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的前进与返回语义
Navigator(Navigator.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 Decorator
NavigationState无法直接被 UI 消费,需要通过toEntries(NavigationState.kt)转换为NavEntry列表:
@Composable fun NavigationState.toEntries( entryProvider: (NavKey) -> NavEntry<NavKey>, ): SnapshotStateList<NavEntry<NavKey>> { val decoratedEntries = subStacks.mapValues { (_, stack) -> val decorators = listOf( rememberSaveableStateHolderNavEntryDecorator<NavKey>(), rememberViewModelStoreNavEntryDecorator<NavKey>(), ) rememberDecoratedNavEntries(stack, decorators, entryProvider) } return topLevelStack.flatMap { decoratedEntries[it] ?: emptyList() }.toMutableStateList() }实现细节:
- 对每个子栈,通过
rememberDecoratedNavEntries创建带装饰器的条目列表; - 两个装饰器分工明确:
rememberSaveableStateHolderNavEntryDecorator负责页面内rememberSaveable状态在导航离开/返回时的保存与恢复,rememberViewModelStoreNavEntryDecorator保证每个导航条目的ViewModelStore生命周期正确(返回销毁、前进重建); - 最终按
topLevelStack的顺序把各子栈条目扁平化,返回 Compose 可观察的SnapshotStateList——当任一栈内容变化时,UI 自动重组。
应用集成:从NiaAppState到NavDisplay
在 app 层,导航状态被封装进NiaAppState(NiaAppState.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 | 同一普通键重复导航不重复入栈(singleTop) |
testNavigateTopLevelSingleTop | 返回当前 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.kt - feature 侧导航键与条目注册示例:
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),仅供参考