Jetpack Compose Navigation 3.0 完全指南
Jetpack Compose 系列第 14 篇,承接上篇的Compose Navigation 2.x,这篇我们来讨论 Compose Navigation 3。
一、前言
Jetpack Navigation最初是为了Fragment体系设计的(如流行的单 Activity + 多 Fragment的SSA 结构),并非 Compose 的原生组件。为了适配声明式 UI,Google 在后续推出了Navigation Compose(基于 Navigation 2.x),但这本质上仍是一种“兼容层”。在实际开发中,我们常常感觉是在“戴着脚镣跳舞”:将命令式的导航思维强行套入声明式架构,难免水土不服。
如果你深度使用过Navigation 2.x(以下简称 Nav2),大概率遇到过下面类似的业务场景:
- 调试时想知道当前栈里有哪些页面,却只能靠猜;
- 想实现一个“返回跳过中间页面”的效果,要在
popUpTo和inclusive之间反复试错; - 想在大屏上同时展示列表和详情,但
NavHost永远只显示一个目的地。
这些结构性问题表明,在 Nav2 上修修补补已无法从根本上解决矛盾——我们需要一套新的、真正为 Compose 而生的导航方案。
2025年, Google 正式发布了Navigation 3(以下简称 Nav3)。请注意,它不是 Nav2 的简单升级,而是一个从底层完全重写的新库。
本文在上一篇的基础上,延伸内容帮大家理解Nav3的设计哲学,并通过代码示例掌握它的核心用法。
截至2026年8月,Navigation 3 已进入稳定版阶段。推荐新项目可以放心使用Nav3。
二、Nav2的痛点
深入了解 Nav3 之前,结合上一篇内容(Jetpack Compose 使用Navigation实现导航)来认识一下 Nav2 的问题——可以帮我们更好的理解 Nav3 为何要这样设计。
2.1 导航栈是黑盒
Nav2 的NavController内部维护着返回栈,程序员只能通过navigate()、popBackStack()等 API 间接管理栈。你没法看到栈里有什么,更没法随心所欲地增删元素。
打比方Nav2是现场导演,必须亲自喊“Action”、喊“Cut”;而 Nav3 像后期剪辑师,剧本(UI State)写好,仅关心帧序列下当前这一帧是什么内容就够了。
2.2 与 Compose 状态模型对着干
Nav2 诞生于 2018年,当时AndroidX都还没普及。它最初是为了XML + Fragment设计的。虽然后来兼容了 Compose ,但底层引擎还是那个老旧的NavController,而不是 Compose 的State。
这就存在一个根本问题:导航状态和UI状态不是同一个“可信源”,没法把导航栈当作 Compose 的State来观察和驱动UI。
2.3 单目的地限制
Nav2 的NavHost一次只能显示一个目的地——栈顶的那个。这意味着在大屏设备上实现“左侧列表 + 右侧详情”的经典布局非常困难,往往需要绕很多弯路。
三、Nav3的进步
Nav 3 从头开始为 Compose 构建,核心思想可以概况为一句话:你拥有返回栈,Nav3只负责渲染它。
在Nav3中,返回栈底层实现是一个普通的SnapshotStateList<T>。你可以随心所欲地add、remove、clear,甚至重新排序。NavDisplay(Nav3 的UI组件)会观察这个列表,并在它变化时自动更新界面。
这个变化怎么强调都不过分。在 View 体系里,导航是"系统/框架替你管理的流程";在 Nav3 里,导航退化成了mutableStateListOf一样的东西——增删改查,所见即所得。
| 维度 | Navigation 2.X | Navigation 3.x |
|---|---|---|
| 返回栈 | 库内部维护,黑盒 | 开发者维护,透明 List |
| 操作方式 | 调用 navigate()、popBackStack() | 直接操作 List(add/remove) |
| 状态来源 | 库内部状态 + 开发者状态,两个来源 | 开发者提供的 State,单一可信来源 |
| 多目的地 | 只显示栈顶一个 | 可同时显示多个(自适应布局) |
| 类型安全 | 2.8.0 后支持,但底层仍基于字符串 | 原生支持,Kotlin 类即路由 |
四、Nav3核心概念速览
Nav3 的核心 API 同样精简,只有四个核心概念:
4.1 NavKey——目的地标识符
NavKey是一个标记接口(Marker Interface),用来标示一个目的地。你的每个页面都需要定义一个实现NavKey的类,并用@Serializable注解。
无参页面使用data object,有参页面用data class:
@SerializabledataobjectHomeKey:NavKey@SerializabledataclassDetailKey(valid:Long):NavKey4.2 NavBackStack——返回栈
NavBackStack本质上就是SnapshotStateList<NavKey>。你可以创建一个mutableStateListOf<NavKey>(),往里面添加或移除元素,就完成了导航。
实际开发按照官方推荐的我们优先使用rememberNavBackStack:
// ⭐rememberNavBackStack 创建一个与Compose生命周期绑定的、可自动保存和恢复的导航返回栈valbackStack=rememberNavBackStack(Routes.HomeKey)rememberNavBackStack帮你做了两件事:
- 用
rememberSaveable让返回栈在旋转屏幕和进程被杀后都能恢复; - 同时返回一个响应式集合——你往里
add一个路由,界面立即重组。
4.3 NavDisplay——渲染器
NavDisplay是 Nav3 的 UI 组件,它观察返回栈——你给它返回栈,它显示栈顶;栈变了,界面跟着变。
4.4 EntryProvider——路由映射
NavGraph的替代品。它回答一个问题:"给定一个路由类型,渲染哪个 Composable?"用 DSL 声明,每个 entry 是一对一映射:
entryProvider=entryProvider{// 路由 → 页面的映射表entry<HomeKey>{HomeScreen(onClick={backStack.add(DetailKey)})// 跳转 = add}entry<DetailKey>{DetailScreen(onBack={backStack.removeLastOrNull()})// 返回 = 弹栈}}五、MVP最小化实战
说了这么多概念,现在看看代码是如何实现的。
下面展示了一个最小化应用:首页–>详情页,并传递参数和执行返回操作。
本篇使用了较新的版本,Nav3 还在不断更新中,也建议大家在真实工作中尽量选择稳定版或团队内部一致的版本。
工程骨架如下:
Nav3Demo/ ├── settings.gradle.kts ├── build.gradle.kts └── app/ ├── build.gradle.kts └── src/main/ ├── AndroidManifest.xml └── java/com/example/nav3demo/MainActivity.kt ← 所有代码都在这里5.1 环境配置
使用版本目录方式添加依赖(打开gradle/libs.versions.toml):
[versions]nav3Core="1.1.5"kotlinxSerializationCore="1.11.0"[libraries]androidx-navigation3-runtime={group="androidx.navigation3",name="navigation3-runtime",version.ref="nav3Core"}androidx-navigation3-ui={group="androidx.navigation3",name="navigation3-ui",version.ref="nav3Core"}kotlinx-serialization-core={group="org.jetbrains.kotlinx",name="kotlinx-serialization-core",version.ref="kotlinxSerializationCore"}在app/build.gradle.kts中应用插件并添加依赖:
plugins{alias(libs.plugins.android.application)alias(libs.plugins.kotlin.compose)alias(libs.plugins.kotlin.serialization)// ⭐类型安全路由需要}android{compileSdk{version=release(36){// ⭐ 编译版本 >= 36minorApiLevel=1}}}dependencies{// ......implementation(libs.androidx.navigation3.runtime)implementation(libs.androidx.navigation3.ui)implementation(libs.kotlinx.serialization.core)}🚨 注意:
compileSdk需要 36 或更高版本。
5.2 定义路由
创建一个Routes.kt文件,定义所有的页面路由:
importandroidx.navigation3.runtime.NavKeyimportkotlinx.serialization.Serializable@SerializabledataobjectHomeKey:NavKey@SerializabledataclassDetailKey(valid:Long,valtitle:String):NavKey🚀 在 Nav2 中,路由是字符串
"detail/{id}",参数解析靠正则或路径匹配,容易出错且不直观。Nav3 直接用 Kotlin数据类,类型安全、IDE 支持完备。
5.3 创建EntryProvider
EntryProvider是一个 DSL,负责将NavKey映射到对应的 Composable 内容。
@ComposablefunAppEntryProvider()=entryProvider{entry<HomeKey>{HomeScreen(onClick={backStack.add(DetailKey(id=1001,title="Nav3 入门"))})}entry<DetailKey>{key->DetailScreen(id=key.id,title=key.title,onBack={backStack.removeLastOrNull()})}}5.4 创建返回栈
在 Activity 或根 Composable 中创建返回栈,并传入NavDisplay:
@ComposablefunNav3App(){// ⭐创建返回栈:传入初始页面,它就是一个"状态列表"valbackStack=rememberNavBackStack(HomeKey)// ⭐NavDisplay 是"渲染器":观察返回栈,渲染栈顶页面NavDisplay(backStack=backStack,onBack={backStack.removeLastOrNull()},// 系统返回键/手势 → 弹栈entryProvider=entryProvider{// 路由 → 页面的映射表entry<HomeKey>{HomeScreen(onClick={backStack.add(DetailKey(id=100L,title="Nav3 入门"))})}entry<DetailKey>{key->DetailScreen(id=key.id,title=key.title,onBack={backStack.removeLastOrNull()})}})}导航就是backStack.add(),返回就是backStack.removeLast()/removeLastOrNull()。没有NavController,没有复杂的 API,就是这么简单直接。
5.5 页面代码
// HomeScreen.kt@ComposableprivatefunHomeScreen(onClick:()->Unit){Column(modifier=Modifier.fillMaxSize().padding(16.dp),verticalArrangement=Arrangement.Center,horizontalAlignment=Alignment.CenterHorizontally){Text("首页")Button(onClick=onClick){Text("去详情页")}}}// DetailScreen.kt@ComposableprivatefunDetailScreen(id:Long,title:String,onBack:()->Unit){Column(modifier=Modifier.fillMaxSize().padding(16.dp),verticalArrangement=Arrangement.Center,horizontalAlignment=Alignment.CenterHorizontally){Text("详情页")Text("id=$id,标题=$title")Button(onClick=onBack){Text("返回")}}}5.6 把一切连起来
最终的MainActivity:
classMainActivity:ComponentActivity(){overridefunonCreate(savedInstanceState:Bundle?){super.onCreate(savedInstanceState)setContent{Nav3App()}}}@ComposablefunNav3App(){valbackStack=rememberNavBackStack(HomeKey)NavDisplay(backStack=backStack,onBack={backStack.removeLastOrNull()},entryProvider=entryProvider{entry<HomeKey>{HomeScreen(onClick={backStack.add(DetailKey(id=100L,title="Nav3 入门"))})}entry<DetailKey>{key->DetailScreen(id=key.id,title=key.title,onBack={backStack.removeLastOrNull()})}})}@ComposableprivatefunHomeScreen(onClick:()->Unit){Column(modifier=Modifier.fillMaxSize().padding(16.dp),verticalArrangement=Arrangement.Center,horizontalAlignment=Alignment.CenterHorizontally){Text("首页")Button(onClick=onClick){Text("去详情页")}}}@ComposableprivatefunDetailScreen(id:Long,title:String,onBack:()->Unit){Column(modifier=Modifier.fillMaxSize().padding(16.dp),verticalArrangement=Arrangement.Center,horizontalAlignment=Alignment.CenterHorizontally){Text("详情页")Text("id=$id,标题=$title")Button(onClick=onBack){Text("返回")}}}运行效果:
六、进阶学习
既然返回栈就是个 List,那我们可以做很多 Nav2 中难以实现的事情。
6.1 跳转时清空中间页面
// 场景:从详情页跳转到新的首页,清空所有历史funnavigateToHomeClearingStack(){backStack.clear()backStack.add(HomeKey)}6.2 防止重复入栈
funnavigateToDetail(id:String,title:String){// 检查栈顶是否已经是这个详情页if(backStack.lastOrNull()isDetailKey){return}backStack.add(DetailKey(id,title))}6.3 批量返回
fungoBack(steps:Int){repeat(steps){if(backStack.size>1){backStack.removeLast()}}}6.4 判断当前页面
valcurrentScreen=backStack.lastOrNull()when(currentScreen){isHomeKey->// 在首页isDetailKey->// 在详情页,可以访问 currentScreen.id}6.5 加入页面动画
// 改动点 1:前进动画——新页从右滑入,旧页向左滑出transitionSpec={slideInHorizontally(initialOffsetX={it})togetherWithslideOutHorizontally(targetOffsetX={-it})},// 改动点 2:返回动画——反过来popTransitionSpec={slideInHorizontally(initialOffsetX={-it})togetherWithslideOutHorizontally(targetOffsetX={it})},七、加入ViewModel
Nav3 提供了与ViewModel的集成方案,可以通过rememberViewModelStoreNavEntryDecorator为每个NavEntry提供独立的 ViewModel 作用域
@ComposablefunMyApp(){valdecorator=rememberViewModelStoreNavEntryDecorator()// 在 NavDisplay 中使用 decorator// 每个 NavEntry 会自动获得独立的 ViewModelStore}这样每个页面的ViewModel生命周期就和它在返回栈中的存在周期绑定了——入栈时创建,出栈时销毁。
八、大屏适配
Nav3 最令人兴奋的特性之一是对自适应布局的原生支持。通过ListDetailSceneStrategy,可以轻松实现“小屏单页、大屏双栏”的效果:
@ComposablefunMyApp(){valbackStack=rememberNavBackStack(HomeKey)valsceneStrategy=rememberListDetailSceneStrategy<Any>()NavDisplay(backStack=backStack,entryProvider=AppEntryProvider(),sceneStrategies=listOf(sceneStrategy),// 自动适配屏幕尺寸)}当屏幕宽度大于某个阈值(通常是 600dp)时,ListDetailSceneStrategy会自动将返回栈中的“列表页”和“详情页”并排显示。这在 Nav2 中几乎不可能优雅地实现。
九、从Nav2迁移注意事项
如果你正在考虑从 Nav2 迁移到 Nav3,以下是关键步骤:
- 添加 Nav3 依赖,移除 Nav2 依赖
- 将字符串路由改为实现
NavKey的@Serializable类 - 将
NavHost+NavGraph替换为NavDisplay+entryProvider - 将导航调用从
navController.navigate()改为backStack.add()
官方提供了详细的迁移指南。
但也泼点冷水:如果你的项目是纯移动端、页面关系简单、已经在 Nav2 上稳定跑了两三年,没必要急着迁移。Nav3 的优势场景是自适应布局、多返回栈、需要深度定制导航行为的应用。技术选型看需求,不看"最新"。
十、总结
Nav3 强调的最重要的一件事:导航不应该是一种特殊的机制,而应该是一种普通的状态。
返回栈从框架黑盒变为开发者可自由操作的SnapshotStateList,导航退化为普通的集合增删,真正实现了“单一可信源”与 Compose 状态模型的无缝对齐。同时原生支持大屏自适应布局(如ListDetailSceneStrategy),大幅降低了多设备适配成本。
同时我们也建议对于新项目或需要复杂导航控制的场景,Nav3 是更优解;但稳定维护的 Nav2 项目可按需迁移,不必盲目追新。
十一、参考资料
- https://developer.android.google.cn/guide/navigation/navigation-3?hl=zh-cn
- https://developer.android.google.cn/guide/navigation/navigation-3/basics?hl=en
- https://android-developers.googleblog.com/2025/05/announcing-jetpack-navigation-3-for-compose.html
- https://developer.android.google.cn/guide/topics/large-screens?hl=zh-cn
- https://developer.android.google.cn/develop/ui/compose/layouts/adaptive/foldables/learn-about-foldables?hl=zh-cn
- https://juejin.cn/post/7654221329193320463?searchId=2026080313224911DC9EC87ED0CDBAFAC6
- https://github.com/android/nav3-recipes/?tab=readme-ov-file#architecture
- https://developer.android.google.cn/reference/kotlin/androidx/compose/material3/adaptive/navigation3/ListDetailSceneStrategy?hl=en
十二、往期系列文章
Jetpack Compose 使用Navigation实现导航
Jetpack Compose 主题与样式
Jetpack Compose 手势处理揭秘
写给新手的 Jetpack Compose 动画手册
Jetpack Compose 副作用
Jetpack Compose 重组机制
Jetpack Compose 状态管理指南
Jetpack Compose 组件大观园
Jetpack Compose 常用UI组件实战演练
Jetpack Compose Modifier 修饰符完全指南:从入门到精通
Jetpack Compose 核心机制:Composable与Modifier
Jetpack Compose 入门指南
Jetpack Compose 前探
