news 2026/9/13 11:49:35

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 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层其他模块(如modeldatadesignsystem)并列,且不直接依赖任何 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-navigation3savedstate-compose作为内部实现细节使用implementation隐藏。这意味着:整个应用对 Navigation 3 的依赖都收敛在:core:navigation模块内,其余模块只需面向Navigator/NavigationState编程,这是模块化架构中隔离框架依赖的典型手法。

双栈模型:一个顶层栈 + 每顶层一份子栈

理解该模块的关键在于其状态结构。NavigationStateNavigationState.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 内的详情页栈)。
  • currentKeycurrentTopLevelKey均通过derivedStateOf派生,保证 Compose 只在相关栈尾变化时重组。
  • 访问currentSubStack时若键不存在会抛出明确错误,帮助在开发期快速暴露状态不一致。

NavKey是 Navigation 3 中的目的地标识。NIA 中每个 feature 模块在api子模块里声明自己的键,例如ForYouNavKey.kt中的@Serializable object ForYouNavKey : NavKeySearchNavKey.kt中的SearchNavKey,而带参数的键(如TopicNavKey,含id)则以@Serializable data class形式携带参数。所有键通过@Serializable注解获得序列化能力,供状态保存/恢复使用。

状态工厂:rememberNavigationState与配置变更/进程死亡恢复

rememberNavigationStateNavigationState.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为每个顶层键各创建一个独立可保存的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) // 普通目的地:压入当前子栈 } }

三个分支对应三种语义:

  1. 重复点击当前顶层目的地clearSubStack():保留子栈根部键,清空其余条目(subList(1, size).clear()),回到该 Tab 的根页面,这是标准的「点击已选 Tab 回首页」行为。
  2. 切换到另一个顶层目的地goToTopLevel(key):若目标是startKeyclear()后只保留它(确保从其他 Tab 回起始页时栈干净);否则remove(key)add(key),实现去重 + 移到末尾的单顶语义。
  3. 普通非顶层目的地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 消费,需要通过toEntriesNavigationState.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 自动重组。

应用集成:从NiaAppStateNavDisplay

在 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 ) }
  • Navigatorremember缓存,与应用状态同生命周期;
  • 底部导航栏每个 item 的onClick统一走navigator.navigate(navKey),选中态由currentTopLevelKey驱动;
  • 各 feature 模块以扩展函数注册条目,例如forYouEntryForYouScreen(onTopicClick = navigator::navigateToTopic)绑定到ForYouNavKey;带参数的topicEntry通过metadata = ListDetailSceneStrategy.detailPane()声明其为「详情面板」目的地,配合rememberListDetailSceneStrategy实现大屏下的 List-Detail 自适应布局,其 ViewModel 以key = id的方式从 Hilt 工厂创建;
  • NavDisplayonBack与系统返回手势绑定,统一交给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.ktNavigator.kt
  • 导航行为测试:NavigatorTest.kt
  • 应用层集成:NiaApp.ktNiaAppState.ktTopLevelNavItem.kt
  • feature 侧导航键与条目注册示例:ForYouNavKey.ktForYouEntryProvider.ktTopicEntryProvider.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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 11:48:45

车载组合导航算法:SINS/GNSS紧耦合与车规级实时实现

简介&#xff1a;本资源是一套面向导航算法研究者与车载系统开发工程师的捷联惯导与组合导航MATLAB仿真代码集&#xff0c;聚焦于SINS/GPS车载组合导航系统的建模、误差补偿与滤波融合实践。资源包含32个文件&#xff0c;主体为30个.m函数脚本&#xff08;如sins.m、kalman.m、…

作者头像 李华
网站建设 2026/9/13 11:47:50

晶振相位噪声:近端与远端噪声的物理机制与协同优化

1. 为什么晶振的“近端”和“远端”噪声不能混为一谈&#xff1f; 你手头那颗标称10ppm、老化率0.5ppm/year的石英晶振&#xff0c;放在频谱仪上一测&#xff0c;相位噪声曲线却在1kHz偏移处突然“翘尾巴”&#xff0c;在100kHz处又莫名抬高——这根本不是数据手册里写的那条平…

作者头像 李华
网站建设 2026/9/13 11:46:53

Simulink光伏MPPT建模:基于单二极管物理模型与S-Function工程实现

简介&#xff1a;本资源是一套基于MATLAB/Simulink构建的独立光伏发电系统建模仿真资料&#xff0c;面向新能源方向本科生、研究生及电力电子初/中级工程师&#xff0c;聚焦光伏系统建模与MPPT控制策略实践。压缩包含10个文件&#xff08;5个Simulink模型.mdl文件用于系统搭建与…

作者头像 李华