【Compose Multiplatform跨端声明式UI实用技术】第10课:CMP 导航——Navigation 3 实战

第9课完成了跨平台 ViewModel 与状态架构的学习。本课将深入 CMP 导航体系的核心------Navigation 3。Navigation 3 不是 Navigation 2 的增量更新,而是一次彻底的重设计:返回栈从库内部状态变为开发者直接持有的 SnapshotStateList,导航操作简化为对列表的增删,自适应多窗格布局成为一等公民。本课将覆盖设计哲学、从 Navigation 2 的迁移路径、基础导航模式、深层链接、跨平台返回手势处理、自适应多窗格布局,以及与 ViewModel 的集成。本课内容基于 CMP 1.12.1、Navigation 3 1.1.1 与 Lifecycle 2.10.0。

目录

  • [一、Navigation 3 的设计哲学](#一、Navigation 3 的设计哲学)
    • [1.1 为什么需要 Navigation 3](#1.1 为什么需要 Navigation 3)
    • [1.2 三大核心原则](#1.2 三大核心原则)
    • [1.3 与 Navigation 2 的核心差异](#1.3 与 Navigation 2 的核心差异)
  • 二、依赖配置与跨平台支持
    • [2.1 添加依赖](#2.1 添加依赖)
    • [2.2 跨平台对齐](#2.2 跨平台对齐)
  • [三、从 Navigation 2 迁移到 Navigation 3](#三、从 Navigation 2 迁移到 Navigation 3)
    • [3.1 迁移概览](#3.1 迁移概览)
    • [3.2 迁移前置条件](#3.2 迁移前置条件)
    • [3.3 概念映射](#3.3 概念映射)
  • 四、基础导航实现
    • [4.1 定义 NavKey](#4.1 定义 NavKey)
    • [4.2 创建返回栈](#4.2 创建返回栈)
    • [4.3 配置 NavDisplay](#4.3 配置 NavDisplay)
    • [4.4 顶层导航与底部导航栏](#4.4 顶层导航与底部导航栏)
  • 五、跨平台返回手势处理
    • [5.1 从 PredictiveBackHandler 迁移](#5.1 从 PredictiveBackHandler 迁移)
    • [5.2 NavigationBackHandler 的使用](#5.2 NavigationBackHandler 的使用)
    • [5.3 各平台返回手势行为](#5.3 各平台返回手势行为)
  • 六、深层链接
    • [6.1 基本概念](#6.1 基本概念)
    • [6.2 实现示例](#6.2 实现示例)
    • [6.3 跨平台注意事项](#6.3 跨平台注意事项)
  • 七、自适应多窗格布局
    • [7.1 Scene 与 SceneStrategy 的概念](#7.1 Scene 与 SceneStrategy 的概念)
    • [7.2 ListDetailSceneStrategy](#7.2 ListDetailSceneStrategy)
    • [7.3 跨平台自适应布局](#7.3 跨平台自适应布局)
  • [八、ViewModel 集成与作用域](#八、ViewModel 集成与作用域)
    • [8.1 将 ViewModel 限定到 NavEntry](#8.1 将 ViewModel 限定到 NavEntry)
    • [8.2 结果传递](#8.2 结果传递)
    • [8.3 与 ViewModel 的配合](#8.3 与 ViewModel 的配合)
  • 九、生产级实践与社区资源
    • [9.1 nav3-recipes 社区仓库](#9.1 nav3-recipes 社区仓库)
    • [9.2 实战示例](#9.2 实战示例)
  • 配套习题(15题)
  • 本课知识点总结

文末附有15道配套习题与本课知识点总结。

一、Navigation 3 的设计哲学

Android 最初的 Navigation 库(通常称为 Nav2)发布于 2018 年,早于 AndroidX 和 Compose。虽然它很好地服务了最初的目标,但官方在收集开发者反馈后发现了两个关键局限:

返回栈状态只能被间接观察。这意味着可能存在两个真相来源(库内部的返回栈和 UI 层对导航状态的理解),潜在地导致应用状态不一致。

NavHost 被设计为仅显示单个目的地------返回栈中最顶层的那个,填满可用空间。这使得实现自适应布局(如大屏幕上的列表-详情双栏布局)变得困难。

1.2 三大核心原则

Navigation 3 建立在三个核心原则之上:

你拥有返回栈 。开发者而非库,拥有和控制返回栈。它是由 Compose 状态支持的简单列表,具体而言,Nav3 期望返回栈是 SnapshotStateList<T>,其中 T 可以是任何类型。通过添加或移除项来导航,状态变化被 Nav3 的 UI 观察和反映。

不挡路。Navigation 3 被设计为开放和可扩展的,提供构建块和有用的默认值。如果需要自定义导航行为,可以下沉到更低层级,创建自己的组件和自定义。

自选构建块。Navigation 3 不将所有行为嵌入库中,而是提供较小的组件,可以组合它们来创建更复杂的功能。官方还提供了"配方书"(nav3-recipes)展示如何组合组件解决常见导航挑战。

维度 Navigation 2 Navigation 3
返回栈 库内部管理,只能间接观察 开发者持有的 SnapshotStateList<T>
导航方式 NavController.navigate() / popBackStack() 对列表的 add() / remove()
路由定义 字符串路由或类型安全路由 NavKey 接口 + @Serializable
目的地渲染 NavHost 的 NavGraph NavDisplay + entryProvider
多窗格布局 实现困难 通过 Scene 和 SceneStrategy 原生支持
返回手势 Android 专属 PredictiveBackHandler 跨平台 NavigationBackHandler
状态作用域 需要手动配置 rememberViewModelStoreNavEntryDecorator

下面是 Navigation 3 三大核心原则的架构图:
#mermaid-svg-4U0jlp7labnmHJkY{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4U0jlp7labnmHJkY .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4U0jlp7labnmHJkY .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4U0jlp7labnmHJkY .error-icon{fill:#552222;}#mermaid-svg-4U0jlp7labnmHJkY .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4U0jlp7labnmHJkY .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4U0jlp7labnmHJkY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4U0jlp7labnmHJkY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4U0jlp7labnmHJkY .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4U0jlp7labnmHJkY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4U0jlp7labnmHJkY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4U0jlp7labnmHJkY .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4U0jlp7labnmHJkY .marker.cross{stroke:#333333;}#mermaid-svg-4U0jlp7labnmHJkY svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4U0jlp7labnmHJkY p{margin:0;}#mermaid-svg-4U0jlp7labnmHJkY .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-4U0jlp7labnmHJkY .cluster-label text{fill:#333;}#mermaid-svg-4U0jlp7labnmHJkY .cluster-label span{color:#333;}#mermaid-svg-4U0jlp7labnmHJkY .cluster-label span p{background-color:transparent;}#mermaid-svg-4U0jlp7labnmHJkY .label text,#mermaid-svg-4U0jlp7labnmHJkY span{fill:#333;color:#333;}#mermaid-svg-4U0jlp7labnmHJkY .node rect,#mermaid-svg-4U0jlp7labnmHJkY .node circle,#mermaid-svg-4U0jlp7labnmHJkY .node ellipse,#mermaid-svg-4U0jlp7labnmHJkY .node polygon,#mermaid-svg-4U0jlp7labnmHJkY .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-4U0jlp7labnmHJkY .rough-node .label text,#mermaid-svg-4U0jlp7labnmHJkY .node .label text,#mermaid-svg-4U0jlp7labnmHJkY .image-shape .label,#mermaid-svg-4U0jlp7labnmHJkY .icon-shape .label{text-anchor:middle;}#mermaid-svg-4U0jlp7labnmHJkY .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-4U0jlp7labnmHJkY .rough-node .label,#mermaid-svg-4U0jlp7labnmHJkY .node .label,#mermaid-svg-4U0jlp7labnmHJkY .image-shape .label,#mermaid-svg-4U0jlp7labnmHJkY .icon-shape .label{text-align:center;}#mermaid-svg-4U0jlp7labnmHJkY .node.clickable{cursor:pointer;}#mermaid-svg-4U0jlp7labnmHJkY .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-4U0jlp7labnmHJkY .arrowheadPath{fill:#333333;}#mermaid-svg-4U0jlp7labnmHJkY .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-4U0jlp7labnmHJkY .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-4U0jlp7labnmHJkY .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4U0jlp7labnmHJkY .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-4U0jlp7labnmHJkY .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4U0jlp7labnmHJkY .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-4U0jlp7labnmHJkY .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-4U0jlp7labnmHJkY .cluster text{fill:#333;}#mermaid-svg-4U0jlp7labnmHJkY .cluster span{color:#333;}#mermaid-svg-4U0jlp7labnmHJkY div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-4U0jlp7labnmHJkY .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-4U0jlp7labnmHJkY rect.text{fill:none;stroke-width:0;}#mermaid-svg-4U0jlp7labnmHJkY .icon-shape,#mermaid-svg-4U0jlp7labnmHJkY .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4U0jlp7labnmHJkY .icon-shape p,#mermaid-svg-4U0jlp7labnmHJkY .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-4U0jlp7labnmHJkY .icon-shape .label rect,#mermaid-svg-4U0jlp7labnmHJkY .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4U0jlp7labnmHJkY .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-4U0jlp7labnmHJkY .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-4U0jlp7labnmHJkY :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Navigation 3 核心原则
你拥有返回栈
不挡路
自选构建块
开发者持有 SnapshotStateList
通过 add/remove 导航
状态变化被 UI 观察
开放可扩展
提供构建块和默认值
可下沉到更低层级自定义
较小组件可组合
nav3-recipes 配方书
组合解决复杂导航挑战

二、依赖配置与跨平台支持

2.1 添加依赖

从 CMP 1.10 开始,Navigation 3 在所有支持平台(Android、iOS、Desktop、Web)的多平台项目中可用。在版本目录中添加:

toml 复制代码
[versions]
multiplatform-nav3-ui = "1.1.1"
compose-multiplatform-lifecycle = "2.10.0"
compose-multiplatform-adaptive = "1.3.0-beta02"

[libraries]
jetbrains-navigation3-ui = { module = "org.jetbrains.androidx.navigation3:navigation3-ui", version.ref = "multiplatform-nav3-ui" }
jetbrains-lifecycle-viewmodelNavigation3 = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "compose-multiplatform-lifecycle" }
jetbrains-material3-adaptiveNavigation3 = { module = "org.jetbrains.compose.material3.adaptive:adaptive-navigation3", version.ref = "compose-multiplatform-adaptive" }
kotlin 复制代码
commonMain.dependencies {
    implementation(libs.jetbrains.navigation3.ui)
    implementation(libs.jetbrains.lifecycle.viewmodelNavigation3)
    implementation(libs.jetbrains.material3.adaptiveNavigation3)
}

Navigation 3 发布为两个构件:navigation3-ui 和 navigation3-common,但只有 navigation3-ui 拥有独立的 CMP 实现,对 navigation3-common 的依赖会通过传递方式添加。

2.2 跨平台对齐

Navigation 3 与 Compose 紧密对齐,Android 导航实现可以最小改动地在共享 CMP 代码中运行。跨平台的核心能力包括:

  • 返回手势自动映射:iOS 边缘滑动、Android 系统返回、桌面 Esc 键均被统一转换为导航到上一个屏幕
  • 浏览器历史集成 :JetBrains 工程师创建的概念验证库 navigation3-browser 将 Nav3 与 Web 浏览器历史记录导航集成
  • iOS 端边缘手势管理 :可通过 EndEdgePanGestureBehavior 选项管理 iOS 上的导航手势

3.1 迁移概览

官方迁移指南列出了从 Navigation 2 迁移到 Navigation 3 的步骤:

  1. 添加 Navigation 3 依赖
  2. 更新导航路由,实现 NavKey 接口
  3. 创建类来持有和修改导航状态
  4. 用这些类替换 NavController
  5. 将目的地从 NavHost 的 NavGraph 移到 entryProvider
  6. 用 NavDisplay 替换 NavHost
  7. 移除 Navigation 2 依赖

3.2 迁移前置条件

  • compileSdk 必须为 36 或更高
  • 目的地必须是 Composable 函数------Navigation 3 专为 Compose 设计
  • 路由必须是强类型的。如果使用字符串路由,需要先迁移到类型安全路由
  • 建议有测试覆盖,验证迁移后导航行为未改变

3.3 概念映射

Navigation 2 Navigation 3
NavController 直接操作 SnapshotStateList<T>
NavHost + NavGraph NavDisplay + entryProvider
NavDestination NavKey + NavEntry
navController.navigate(route) backStack.add(route)
navController.popBackStack() backStack.removeLastOrNull()
composable<Route> { } entry<Route> { }

下面是 Navigation 2 迁移到 Navigation 3 的完整流程图:
#mermaid-svg-XXdGNfgbWbm3Ux80{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-XXdGNfgbWbm3Ux80 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-XXdGNfgbWbm3Ux80 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-XXdGNfgbWbm3Ux80 .error-icon{fill:#552222;}#mermaid-svg-XXdGNfgbWbm3Ux80 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XXdGNfgbWbm3Ux80 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-XXdGNfgbWbm3Ux80 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XXdGNfgbWbm3Ux80 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XXdGNfgbWbm3Ux80 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-XXdGNfgbWbm3Ux80 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XXdGNfgbWbm3Ux80 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XXdGNfgbWbm3Ux80 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XXdGNfgbWbm3Ux80 .marker.cross{stroke:#333333;}#mermaid-svg-XXdGNfgbWbm3Ux80 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XXdGNfgbWbm3Ux80 p{margin:0;}#mermaid-svg-XXdGNfgbWbm3Ux80 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-XXdGNfgbWbm3Ux80 .cluster-label text{fill:#333;}#mermaid-svg-XXdGNfgbWbm3Ux80 .cluster-label span{color:#333;}#mermaid-svg-XXdGNfgbWbm3Ux80 .cluster-label span p{background-color:transparent;}#mermaid-svg-XXdGNfgbWbm3Ux80 .label text,#mermaid-svg-XXdGNfgbWbm3Ux80 span{fill:#333;color:#333;}#mermaid-svg-XXdGNfgbWbm3Ux80 .node rect,#mermaid-svg-XXdGNfgbWbm3Ux80 .node circle,#mermaid-svg-XXdGNfgbWbm3Ux80 .node ellipse,#mermaid-svg-XXdGNfgbWbm3Ux80 .node polygon,#mermaid-svg-XXdGNfgbWbm3Ux80 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-XXdGNfgbWbm3Ux80 .rough-node .label text,#mermaid-svg-XXdGNfgbWbm3Ux80 .node .label text,#mermaid-svg-XXdGNfgbWbm3Ux80 .image-shape .label,#mermaid-svg-XXdGNfgbWbm3Ux80 .icon-shape .label{text-anchor:middle;}#mermaid-svg-XXdGNfgbWbm3Ux80 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-XXdGNfgbWbm3Ux80 .rough-node .label,#mermaid-svg-XXdGNfgbWbm3Ux80 .node .label,#mermaid-svg-XXdGNfgbWbm3Ux80 .image-shape .label,#mermaid-svg-XXdGNfgbWbm3Ux80 .icon-shape .label{text-align:center;}#mermaid-svg-XXdGNfgbWbm3Ux80 .node.clickable{cursor:pointer;}#mermaid-svg-XXdGNfgbWbm3Ux80 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-XXdGNfgbWbm3Ux80 .arrowheadPath{fill:#333333;}#mermaid-svg-XXdGNfgbWbm3Ux80 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-XXdGNfgbWbm3Ux80 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-XXdGNfgbWbm3Ux80 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XXdGNfgbWbm3Ux80 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-XXdGNfgbWbm3Ux80 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XXdGNfgbWbm3Ux80 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-XXdGNfgbWbm3Ux80 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-XXdGNfgbWbm3Ux80 .cluster text{fill:#333;}#mermaid-svg-XXdGNfgbWbm3Ux80 .cluster span{color:#333;}#mermaid-svg-XXdGNfgbWbm3Ux80 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-XXdGNfgbWbm3Ux80 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-XXdGNfgbWbm3Ux80 rect.text{fill:none;stroke-width:0;}#mermaid-svg-XXdGNfgbWbm3Ux80 .icon-shape,#mermaid-svg-XXdGNfgbWbm3Ux80 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XXdGNfgbWbm3Ux80 .icon-shape p,#mermaid-svg-XXdGNfgbWbm3Ux80 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-XXdGNfgbWbm3Ux80 .icon-shape .label rect,#mermaid-svg-XXdGNfgbWbm3Ux80 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XXdGNfgbWbm3Ux80 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-XXdGNfgbWbm3Ux80 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-XXdGNfgbWbm3Ux80 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Navigation 2 项目
添加 Navigation 3 依赖
更新路由,实现 NavKey 接口
创建类持有导航状态
替换 NavController
目的地移入 entryProvider
NavHost 替换为 NavDisplay
移除 Navigation 2 依赖
Navigation 3 项目

四、基础导航实现

每个屏幕由一个强类型、可序列化的对象或 data class 标识,实现 NavKey 接口:

kotlin 复制代码
@Serializable
sealed interface Screen : NavKey {
    @Serializable
    data object Home : Screen

    @Serializable
    data class Details(val itemId: String) : Screen

    @Serializable
    data object Settings : Screen
}

@Serializable 注解使返回栈可以持久化,在配置更改和进程终止后恢复。

4.2 创建返回栈

kotlin 复制代码
val backStack = rememberNavBackStack(Screen.Home)

rememberNavBackStack 创建一个 SnapshotStateList 并在配置更改和进程终止后持久化它。

NavDisplay 是核心 UI 组件,观察返回栈并渲染活动屏幕:

kotlin 复制代码
NavDisplay(
    backStack = backStack,
    onBack = { count -> backStack.removeLastOrNull() },
    entryDecorators = listOf(
        rememberSaveableStateHolderNavEntryDecorator(),
        rememberViewModelStoreNavEntryDecorator(), // ViewModel 作用域限定到每个 entry
    ),
    entryProvider = entryProvider {
        entry<Screen.Home> {
            HomeScreen(
                onNavigateToDetails = { id -> backStack.add(Screen.Details(id)) }
            )
        }
        entry<Screen.Details> { key ->
            DetailsScreen(
                itemId = key.itemId,
                onBack = { backStack.removeLastOrNull() }
            )
        }
        entry<Screen.Settings> {
            SettingsScreen()
        }
    }
)

关键点 :entryProvider 的 DSL 中,entry<Key> { } 的 lambda 接收类型化的 key。导航即 backStack.add(),返回即 backStack.removeLastOrNull()。

4.4 顶层导航与底部导航栏

对于带有底部导航栏的应用,可以定义顶层路由并在返回栈切换时保留状态:

kotlin 复制代码
val topLevelRoutes = listOf(Screen.Home, Screen.Settings)

// 底部导航栏切换逻辑
fun navigateToTopLevel(route: Screen) {
    backStack.clear()
    backStack.add(route)
}

下面是 Navigation 3 基础导航的完整工作流程:
#mermaid-svg-w2wCoItLO7pE2Hh4{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-w2wCoItLO7pE2Hh4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-w2wCoItLO7pE2Hh4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-w2wCoItLO7pE2Hh4 .error-icon{fill:#552222;}#mermaid-svg-w2wCoItLO7pE2Hh4 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-w2wCoItLO7pE2Hh4 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-w2wCoItLO7pE2Hh4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-w2wCoItLO7pE2Hh4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-w2wCoItLO7pE2Hh4 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-w2wCoItLO7pE2Hh4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-w2wCoItLO7pE2Hh4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-w2wCoItLO7pE2Hh4 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-w2wCoItLO7pE2Hh4 .marker.cross{stroke:#333333;}#mermaid-svg-w2wCoItLO7pE2Hh4 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-w2wCoItLO7pE2Hh4 p{margin:0;}#mermaid-svg-w2wCoItLO7pE2Hh4 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-w2wCoItLO7pE2Hh4 .cluster-label text{fill:#333;}#mermaid-svg-w2wCoItLO7pE2Hh4 .cluster-label span{color:#333;}#mermaid-svg-w2wCoItLO7pE2Hh4 .cluster-label span p{background-color:transparent;}#mermaid-svg-w2wCoItLO7pE2Hh4 .label text,#mermaid-svg-w2wCoItLO7pE2Hh4 span{fill:#333;color:#333;}#mermaid-svg-w2wCoItLO7pE2Hh4 .node rect,#mermaid-svg-w2wCoItLO7pE2Hh4 .node circle,#mermaid-svg-w2wCoItLO7pE2Hh4 .node ellipse,#mermaid-svg-w2wCoItLO7pE2Hh4 .node polygon,#mermaid-svg-w2wCoItLO7pE2Hh4 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-w2wCoItLO7pE2Hh4 .rough-node .label text,#mermaid-svg-w2wCoItLO7pE2Hh4 .node .label text,#mermaid-svg-w2wCoItLO7pE2Hh4 .image-shape .label,#mermaid-svg-w2wCoItLO7pE2Hh4 .icon-shape .label{text-anchor:middle;}#mermaid-svg-w2wCoItLO7pE2Hh4 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-w2wCoItLO7pE2Hh4 .rough-node .label,#mermaid-svg-w2wCoItLO7pE2Hh4 .node .label,#mermaid-svg-w2wCoItLO7pE2Hh4 .image-shape .label,#mermaid-svg-w2wCoItLO7pE2Hh4 .icon-shape .label{text-align:center;}#mermaid-svg-w2wCoItLO7pE2Hh4 .node.clickable{cursor:pointer;}#mermaid-svg-w2wCoItLO7pE2Hh4 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-w2wCoItLO7pE2Hh4 .arrowheadPath{fill:#333333;}#mermaid-svg-w2wCoItLO7pE2Hh4 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-w2wCoItLO7pE2Hh4 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-w2wCoItLO7pE2Hh4 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-w2wCoItLO7pE2Hh4 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-w2wCoItLO7pE2Hh4 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-w2wCoItLO7pE2Hh4 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-w2wCoItLO7pE2Hh4 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-w2wCoItLO7pE2Hh4 .cluster text{fill:#333;}#mermaid-svg-w2wCoItLO7pE2Hh4 .cluster span{color:#333;}#mermaid-svg-w2wCoItLO7pE2Hh4 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-w2wCoItLO7pE2Hh4 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-w2wCoItLO7pE2Hh4 rect.text{fill:none;stroke-width:0;}#mermaid-svg-w2wCoItLO7pE2Hh4 .icon-shape,#mermaid-svg-w2wCoItLO7pE2Hh4 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-w2wCoItLO7pE2Hh4 .icon-shape p,#mermaid-svg-w2wCoItLO7pE2Hh4 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-w2wCoItLO7pE2Hh4 .icon-shape .label rect,#mermaid-svg-w2wCoItLO7pE2Hh4 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-w2wCoItLO7pE2Hh4 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-w2wCoItLO7pE2Hh4 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-w2wCoItLO7pE2Hh4 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 定义 NavKey
创建返回栈
配置 NavDisplay
entryProvider 映射
entry
entry
entry
HomeScreen
DetailsScreen
SettingsScreen
backStack.add() 导航
backStack.removeLastOrNull() 返回

五、跨平台返回手势处理

5.1 从 PredictiveBackHandler 迁移

CMP 中曾引入 PredictiveBackHandler() 将 Android 原生预测性返回手势带到其他平台。随着 Navigation 3 的发布,旧实现已被弃用,取而代之的是 NavigationBackHandler()。

NavigationBackHandler 包装了更通用的 NavigationEventHandler 实现:

kotlin 复制代码
val navState = rememberNavigationEventState(NavigationEventInfo.None)

NavigationBackHandler(
    state = navState,
    isBackEnabled = true,
    onBackCancelled = {
        // 处理已取消的返回手势
    },
    onBackCompleted = {
        // 处理已完成的返回手势
    }
)

LaunchedEffect(navState.transitionState) {
    val transitionState = navState.transitionState
    if (transitionState is NavigationEventTransitionState.InProgress) {
        val progress = transitionState.latestEvent.progress
        // 根据手势进度制作动画
    }
}

迁移要点 :state 参数是强制的,如果没有要存储的信息可使用 NavigationEventInfo.None 作为存根;onBack 参数已拆分为 onBackCancelled 和 onBackCompleted。

5.3 各平台返回手势行为

平台 返回手势 说明
Android 系统返回按钮 / 边缘滑动 支持预测性返回动画
iOS 屏幕边缘滑动 可通过 EndEdgePanGestureBehavior 配置(默认 Disabled)
Desktop Esc 键 无触控手势支持
Web 浏览器后退按钮 需配合 navigation3-browser 库

六、深层链接

6.1 基本概念

从 Navigation 3 1.2.0 开始,支持通过 DeepLinkRequest 和 DeepLinkMatcher 实现深层链接:

  • DeepLinkRequest :包含 DeepLinkUri 和可选的 mimeType 或 action(仅 Android)
  • DeepLinkMatcher :将传入请求映射到导航键,每个匹配实例关联一个导航键
  • UriDeepLinkMatcher :基于 URI 模式匹配,支持原始类型参数和集合类型参数

6.2 实现示例

kotlin 复制代码
@Serializable
data class UserKey(val id: Int) : NavKey

val userMatcher = UriDeepLinkMatcher(
    uriPattern = "www.sampledeeplink.com/user?userId={id}",
    serializer = serializer<UserKey>()
)

// 处理请求,带兜底键
val key = userMatcher.match(deepLinkRequest)?.key ?: Screen.Home

6.3 跨平台注意事项

在 Android 上,需要在 AndroidManifest.xml 中定义 intent filters 来指定应用能处理的 URI;在 onCreate 或 onNewIntent 中匹配传入请求并更新返回栈。CMP 提供了 DeepLinkUri 作为 android.net.Uri 的多平台版本。

七、自适应多窗格布局

7.1 Scene 与 SceneStrategy 的概念

Navigation 3 引入了强大的布局系统,通过 Scene 和 SceneStrategy 管理界面流程。

Scene 是呈现一个或多个 NavEntry 实例的基本单元,可以理解为界面的一个视觉状态或部分。每个 Scene 由 key 和类唯一标识,包含 entries(要显示的 NavEntry 列表)、previousEntries(返回时产生的 NavEntry)和 content(如何渲染)。

SceneStrategy 决定如何排列返回栈中的 NavEntry 列表并转换为 Scene。核心方法是 calculateScene,如果不能处理给定条目则返回 null,让其他策略有机会创建 Scene。

7.2 ListDetailSceneStrategy

Material 3 Adaptive 提供了 ListDetailSceneStrategy,将 NavEntry 排列为自适应的 ListDetailPaneScaffold。通过在 NavEntry 的 metadata 中使用 listPane、detailPane 或 extraPane,条目可被分配到列表窗格、详情窗格或额外窗格。当窗口足够大时,这些窗格会并排显示;窗口尺寸变化时自动适配。

kotlin 复制代码
val sceneStrategy = rememberListDetailSceneStrategy<Any>()

NavDisplay(
    backStack = backStack,
    sceneStrategies = listOf(sceneStrategy),
    entryProvider = entryProvider {
        entry<ListKey>(
            metadata = ListDetailSceneStrategy.listPane(
                detailPlaceholder = { DetailPaneContent(selectedItem = null) }
            )
        ) {
            ListPaneContent(items = items, onItemClick = { /* ... */ })
        }
        entry<DetailKey>(
            metadata = ListDetailSceneStrategy.detailPane()
        ) { key ->
            DetailPaneContent(selectedItem = key.item)
        }
    }
)

7.3 跨平台自适应布局

Navigation 3 的自适应布局系统在所有平台上工作。在手机上,列表和详情可能分别占据全屏(通过返回栈切换);在平板或桌面上,它们可以并排显示。这种自适应切换由 SceneStrategy 自动处理,开发者无需手动判断屏幕尺寸。

下面是 Navigation 3 自适应多窗格布局的架构图:
#mermaid-svg-MnGo1xBsA3Mu29am{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-MnGo1xBsA3Mu29am .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-MnGo1xBsA3Mu29am .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-MnGo1xBsA3Mu29am .error-icon{fill:#552222;}#mermaid-svg-MnGo1xBsA3Mu29am .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-MnGo1xBsA3Mu29am .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-MnGo1xBsA3Mu29am .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-MnGo1xBsA3Mu29am .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-MnGo1xBsA3Mu29am .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-MnGo1xBsA3Mu29am .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-MnGo1xBsA3Mu29am .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-MnGo1xBsA3Mu29am .marker{fill:#333333;stroke:#333333;}#mermaid-svg-MnGo1xBsA3Mu29am .marker.cross{stroke:#333333;}#mermaid-svg-MnGo1xBsA3Mu29am svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-MnGo1xBsA3Mu29am p{margin:0;}#mermaid-svg-MnGo1xBsA3Mu29am .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-MnGo1xBsA3Mu29am .cluster-label text{fill:#333;}#mermaid-svg-MnGo1xBsA3Mu29am .cluster-label span{color:#333;}#mermaid-svg-MnGo1xBsA3Mu29am .cluster-label span p{background-color:transparent;}#mermaid-svg-MnGo1xBsA3Mu29am .label text,#mermaid-svg-MnGo1xBsA3Mu29am span{fill:#333;color:#333;}#mermaid-svg-MnGo1xBsA3Mu29am .node rect,#mermaid-svg-MnGo1xBsA3Mu29am .node circle,#mermaid-svg-MnGo1xBsA3Mu29am .node ellipse,#mermaid-svg-MnGo1xBsA3Mu29am .node polygon,#mermaid-svg-MnGo1xBsA3Mu29am .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-MnGo1xBsA3Mu29am .rough-node .label text,#mermaid-svg-MnGo1xBsA3Mu29am .node .label text,#mermaid-svg-MnGo1xBsA3Mu29am .image-shape .label,#mermaid-svg-MnGo1xBsA3Mu29am .icon-shape .label{text-anchor:middle;}#mermaid-svg-MnGo1xBsA3Mu29am .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-MnGo1xBsA3Mu29am .rough-node .label,#mermaid-svg-MnGo1xBsA3Mu29am .node .label,#mermaid-svg-MnGo1xBsA3Mu29am .image-shape .label,#mermaid-svg-MnGo1xBsA3Mu29am .icon-shape .label{text-align:center;}#mermaid-svg-MnGo1xBsA3Mu29am .node.clickable{cursor:pointer;}#mermaid-svg-MnGo1xBsA3Mu29am .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-MnGo1xBsA3Mu29am .arrowheadPath{fill:#333333;}#mermaid-svg-MnGo1xBsA3Mu29am .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-MnGo1xBsA3Mu29am .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-MnGo1xBsA3Mu29am .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MnGo1xBsA3Mu29am .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-MnGo1xBsA3Mu29am .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MnGo1xBsA3Mu29am .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-MnGo1xBsA3Mu29am .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-MnGo1xBsA3Mu29am .cluster text{fill:#333;}#mermaid-svg-MnGo1xBsA3Mu29am .cluster span{color:#333;}#mermaid-svg-MnGo1xBsA3Mu29am div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-MnGo1xBsA3Mu29am .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-MnGo1xBsA3Mu29am rect.text{fill:none;stroke-width:0;}#mermaid-svg-MnGo1xBsA3Mu29am .icon-shape,#mermaid-svg-MnGo1xBsA3Mu29am .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MnGo1xBsA3Mu29am .icon-shape p,#mermaid-svg-MnGo1xBsA3Mu29am .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-MnGo1xBsA3Mu29am .icon-shape .label rect,#mermaid-svg-MnGo1xBsA3Mu29am .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MnGo1xBsA3Mu29am .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-MnGo1xBsA3Mu29am .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-MnGo1xBsA3Mu29am :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 手机(小屏)
平板/桌面(大屏)
NavDisplay
SceneStrategy
ListDetailSceneStrategy
calculateScene 方法
窗口尺寸判断
列表全屏显示
列表+详情并排显示
通过返回栈切换
ListDetailPaneScaffold
listPane 窗格
detailPane 窗格
extraPane 窗格

八、ViewModel 集成与作用域

在第9课中,我们讨论了 ViewModel 作用域的重要性。在 Navigation 3 中,通过 rememberViewModelStoreNavEntryDecorator() 将 ViewModel 实例限定到各个屏幕(entry):

kotlin 复制代码
NavDisplay(
    backStack = backStack,
    entryDecorators = listOf(
        rememberSaveableStateHolderNavEntryDecorator(),
        rememberViewModelStoreNavEntryDecorator(),
    ),
    entryProvider = entryProvider { /* ... */ }
)

关键约束 :rememberViewModelStoreNavEntryDecorator() 需要配合 rememberSaveableStateHolderNavEntryDecorator() 使用,以确保 ViewModel 能正确访问 SavedStateHandle。

8.2 结果传递

从 Navigation 3 1.2.0 开始,可以通过 ResultEventBus 在目的地之间返回结果:

kotlin 复制代码
// 产生结果的屏幕
entry<AddressPickerRoute> {
    AddressPickerScreen(
        onAddressSelected = { address ->
            resultBus.sendResult(
                resultKey = "pickup_address",
                result = address
            )
        }
    )
}

// 接收结果的屏幕
val address by resultBus.conflateAsState<Address>("pickup_address")

ResultEventBus 使用键来标识和路由每个结果,支持基于事件的效果或基于状态的可观察对象两种消费方式。

8.3 与 ViewModel 的配合

NavEntry 作用域的 ViewModel 与 NavEntry 生命周期绑定------当 NavEntry 从返回栈中弹出时,其 ViewModel 的 onCleared() 被调用。这与第9课中讨论的 ViewModel 作用域原则完全一致。

下面是 Navigation 3 中 ViewModel 集成与结果传递的架构图:
#mermaid-svg-X98xHS68Arz9mnOc{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-X98xHS68Arz9mnOc .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-X98xHS68Arz9mnOc .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-X98xHS68Arz9mnOc .error-icon{fill:#552222;}#mermaid-svg-X98xHS68Arz9mnOc .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-X98xHS68Arz9mnOc .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-X98xHS68Arz9mnOc .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-X98xHS68Arz9mnOc .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-X98xHS68Arz9mnOc .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-X98xHS68Arz9mnOc .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-X98xHS68Arz9mnOc .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-X98xHS68Arz9mnOc .marker{fill:#333333;stroke:#333333;}#mermaid-svg-X98xHS68Arz9mnOc .marker.cross{stroke:#333333;}#mermaid-svg-X98xHS68Arz9mnOc svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-X98xHS68Arz9mnOc p{margin:0;}#mermaid-svg-X98xHS68Arz9mnOc .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-X98xHS68Arz9mnOc .cluster-label text{fill:#333;}#mermaid-svg-X98xHS68Arz9mnOc .cluster-label span{color:#333;}#mermaid-svg-X98xHS68Arz9mnOc .cluster-label span p{background-color:transparent;}#mermaid-svg-X98xHS68Arz9mnOc .label text,#mermaid-svg-X98xHS68Arz9mnOc span{fill:#333;color:#333;}#mermaid-svg-X98xHS68Arz9mnOc .node rect,#mermaid-svg-X98xHS68Arz9mnOc .node circle,#mermaid-svg-X98xHS68Arz9mnOc .node ellipse,#mermaid-svg-X98xHS68Arz9mnOc .node polygon,#mermaid-svg-X98xHS68Arz9mnOc .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-X98xHS68Arz9mnOc .rough-node .label text,#mermaid-svg-X98xHS68Arz9mnOc .node .label text,#mermaid-svg-X98xHS68Arz9mnOc .image-shape .label,#mermaid-svg-X98xHS68Arz9mnOc .icon-shape .label{text-anchor:middle;}#mermaid-svg-X98xHS68Arz9mnOc .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-X98xHS68Arz9mnOc .rough-node .label,#mermaid-svg-X98xHS68Arz9mnOc .node .label,#mermaid-svg-X98xHS68Arz9mnOc .image-shape .label,#mermaid-svg-X98xHS68Arz9mnOc .icon-shape .label{text-align:center;}#mermaid-svg-X98xHS68Arz9mnOc .node.clickable{cursor:pointer;}#mermaid-svg-X98xHS68Arz9mnOc .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-X98xHS68Arz9mnOc .arrowheadPath{fill:#333333;}#mermaid-svg-X98xHS68Arz9mnOc .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-X98xHS68Arz9mnOc .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-X98xHS68Arz9mnOc .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-X98xHS68Arz9mnOc .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-X98xHS68Arz9mnOc .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-X98xHS68Arz9mnOc .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-X98xHS68Arz9mnOc .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-X98xHS68Arz9mnOc .cluster text{fill:#333;}#mermaid-svg-X98xHS68Arz9mnOc .cluster span{color:#333;}#mermaid-svg-X98xHS68Arz9mnOc div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-X98xHS68Arz9mnOc .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-X98xHS68Arz9mnOc rect.text{fill:none;stroke-width:0;}#mermaid-svg-X98xHS68Arz9mnOc .icon-shape,#mermaid-svg-X98xHS68Arz9mnOc .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-X98xHS68Arz9mnOc .icon-shape p,#mermaid-svg-X98xHS68Arz9mnOc .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-X98xHS68Arz9mnOc .icon-shape .label rect,#mermaid-svg-X98xHS68Arz9mnOc .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-X98xHS68Arz9mnOc .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-X98xHS68Arz9mnOc .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-X98xHS68Arz9mnOc :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} NavDisplay
entryDecorators
rememberSaveableStateHolderNavEntryDecorator
rememberViewModelStoreNavEntryDecorator
SavedStateHandle 支持
ViewModel 限定到 NavEntry
ViewModel 生命周期绑定
NavEntry 弹出时 onCleared
ResultEventBus
sendResult 发送结果
conflateAsState 接收结果

九、生产级实践与社区资源

官方维护的 nav3-recipes 仓库(github.com/android/nav3-recipes)展示了如何组合组件解决常见导航挑战,包括多步返回导航、屏幕状态保留、返回结果传递等。CMP 社区也有专门的实现仓库,如 terrakok/nav3-recipes,展示了 CMP 平台上的常见用例实现。

9.2 实战示例

FantasyPremierLeague 是一个完整的 CMP 示例应用,展示了 Navigation 3 在生产级项目中的使用方式------定义顶层路由、创建返回栈、使用 NavDisplay 管理 key/content 映射,以及 Material 3 Adaptive Navigation3 的自适应布局集成。

RealWorld CMP Nav3 MVIKotlin 是一个更复杂的示例,结合了 CMP、Navigation 3、原生 CMP ViewModel 和 MVIKotlin,展示了 MVI 架构与 Navigation 3 的协作方式。

配套习题(15题)

第一组:设计哲学与核心概念(4题)

第1题 Navigation 3 中,返回栈的推荐类型是( )

A. List<T>

B. MutableList<T>

C. SnapshotStateList<T>

D. ArrayDeque<T>

答案:C

解析 :Navigation 3 期望返回栈是 SnapshotStateList<T>,其中 T 可以是任何类型。通过添加或移除项来导航,状态变化被 Nav3 的 UI 观察和反映。

第2题 Navigation 3 的三大核心原则不包括( )

A. 你拥有返回栈

B. 不挡路,开放可扩展

C. 自动管理所有导航状态

D. 自选构建块

答案:C

解析:Navigation 3 的三大核心原则是:你拥有返回栈、不挡路(开放可扩展)、自选构建块。Navigation 3 不自动管理导航状态,而是将控制权交给开发者。

第3题 与 Navigation 2 相比,Navigation 3 在多窗格布局方面的关键改进是( )

A. 增加了新的 NavHost 参数

B. 通过 Scene 和 SceneStrategy 原生支持自适应多窗格布局

C. 强制使用单窗格布局

D. 仅支持 Android 平台的多窗格

答案:B

解析:Navigation 3 引入 Scene 和 SceneStrategy,原生支持自适应多窗格布局。Nav2 的 NavHost 仅显示单个目的地,使多窗格布局实现困难。

第4题 Navigation 3 从哪个版本开始支持在 CMP 所有平台上使用?( )

A. CMP 1.8.0

B. CMP 1.9.0

C. CMP 1.10.0

D. CMP 1.12.0

答案:C

解析:从 CMP 1.10 开始,Navigation 3 在所有支持平台(Android、iOS、Desktop、Web)的多平台项目中可用。

第二组:基础导航与迁移(4题)

第5题 在 Navigation 3 中,导航到新屏幕的操作是( )

A. navController.navigate(route)

B. backStack.add(route)

C. navigateTo(route)

D. pushRoute(route)

答案:B

解析 :Navigation 3 中,导航即对返回栈列表的 add() 操作,返回即 removeLastOrNull()。不再需要 NavController。

第6题 NavDisplay 的 entryProvider 参数的作用是( )

A. 定义返回栈的初始状态

B. 将 NavKey 映射到对应的 Composable UI

C. 配置动画

D. 管理 ViewModel

答案:B

解析 :entryProvider 是一个映射 DSL,将 NavKey 链接到其实际的 @Composable UI。entry<Key> { } 中 Key 是类型化的返回栈元素。

第7题 从 Navigation 2 迁移到 Navigation 3 时,NavHost 应替换为( )

A. NavController

B. NavDisplay

C. NavGraph

D. NavDestination

答案:B

解析 :迁移步骤中,用 NavDisplay 替换 NavHost,将目的地从 NavHost 的 NavGraph 移到 entryProvider。

第8题 定义 NavKey 时必须添加的注解是( )

A. @Composable

B. @Serializable

C. @Immutable

D. @Stable

答案:B

解析 :@Serializable 注解使返回栈可以持久化,在配置更改和进程终止后恢复。NavKey 通常是 @Serializable 的 data class 或 data object。

第三组:返回手势与深层链接(4题)

第9题 CMP 中已弃用的预测性返回手势 API 是( )

A. NavigationBackHandler()

B. PredictiveBackHandler()

C. BackHandler()

D. NavigationEventHandler()

答案:B

解析 :随着 Navigation 3 的发布,PredictiveBackHandler() 已被弃用,取而代之的是 NavigationBackHandler(),它包装了更通用的 NavigationEventHandler() 实现。

第10题 NavigationBackHandler 中 onBack 参数已被拆分为( )

A. onBackStart 和 onBackEnd

B. onBackCancelled 和 onBackCompleted

C. onBackPressed 和 onBackReleased

D. onBackBegin 和 onBackFinish

答案:B

解析 :onBack 参数已拆分为 onBackCancelled 和 onBackCompleted,因此无需单独跟踪取消的手势。

第11题 在 Navigation 3 中,DeepLinkRequest 包含的信息不包括( )

A. DeepLinkUri

B. mimeType

C. action(仅 Android)

D. backStack

答案:D

解析 :DeepLinkRequest 包含 DeepLinkUri 和可选的 mimeType 或 action(仅 Android)。backStack 是导航状态,不属于深层链接请求。

第12题 UriDeepLinkMatcher 的作用是( )

A. 创建新的导航键

B. 基于 URI 模式匹配,将请求映射到导航键

C. 管理返回栈

D. 渲染 UI

答案:B

解析 :UriDeepLinkMatcher 是基于 URI 模式匹配的 DeepLinkMatcher 实现,支持原始类型参数和集合类型参数,将请求映射到导航键。

第四组:自适应布局与 ViewModel(3题)

第13题 在 Navigation 3 中,SceneStrategy 的核心方法是( )

A. createScene()

B. calculateScene()

C. buildScene()

D. renderScene()

答案:B

解析 :SceneStrategy 的核心是 calculateScene() 方法,它从返回栈获取当前 NavEntry 列表,如果能成功形成 Scene 则返回,否则返回 null。

第14题 将 ViewModel 作用域限定到 Navigation 3 的各个 entry 需要使用的装饰器是( )

A. rememberSaveableStateHolderNavEntryDecorator()

B. rememberViewModelStoreNavEntryDecorator()

C. rememberNavigationBackHandler()

D. rememberSceneStrategy()

答案:B

解析 :rememberViewModelStoreNavEntryDecorator() 将 ViewModel 实例限定到各个屏幕(entry)。需要配合 rememberSaveableStateHolderNavEntryDecorator() 使用以确保 SavedStateHandle 可用。

第15题 Navigation 3 中通过 ResultEventBus 返回结果时,接收结果的方式是( )

A. 直接读取返回栈

B. 使用 conflateAsState() 观察最新结果

C. 通过 Intent 传递

D. 使用 rememberUpdatedState

答案:B

解析 :ResultEventBus 使用键来标识和路由结果,接收方通过 conflateAsState() 将最新结果作为状态观察,Compose 在结果更新时自动重组。

本课知识点总结

Navigation 3 建立在三大核心原则之上:你拥有返回栈 (开发者持有 SnapshotStateList<T>,通过增删操作导航)、不挡路 (开放可扩展,提供构建块和默认值)、自选构建块(较小组件可组合)。与 Navigation 2 的根本差异在于返回栈的所有权和可见性。

2. 依赖配置与跨平台支持

CMP 1.10 起 Navigation 3 在所有平台可用。核心依赖为 org.jetbrains.androidx.navigation3:navigation3-ui:1.1.1,配合 ViewModel 和 Adaptive 库需要额外添加 lifecycle-viewmodel-navigation3 和 adaptive-navigation3。跨平台核心能力包括返回手势自动映射和浏览器历史集成。

3. 迁移与基础用法

从 Navigation 2 迁移的核心步骤:添加依赖 → 路由实现 NavKey → 创建返回栈 → 替换 NavController → 目的地移入 entryProvider → NavHost 替换为 NavDisplay。基础模式:@Serializable 定义 NavKey,rememberNavBackStack() 创建返回栈,NavDisplay + entryProvider 渲染。导航即 backStack.add(),返回即 backStack.removeLastOrNull()。

4. 跨平台返回手势与深层链接

PredictiveBackHandler() 已弃用,改用 NavigationBackHandler(),onBack 拆分为 onBackCancelled 和 onBackCompleted。各平台返回手势:Android 系统返回、iOS 边缘滑动、Desktop Esc 键、Web 浏览器后退。深层链接通过 DeepLinkRequest 和 DeepLinkMatcher 实现,UriDeepLinkMatcher 支持 URI 模式匹配。

5. 自适应布局与 Scene 系统

Scene 是呈现 NavEntry 的基本单元,SceneStrategy 决定如何排列 NavEntry 并转换为 Scene。ListDetailSceneStrategy 通过 metadata 中的 listPane/detailPane 将条目分配到不同窗格,窗口足够大时并排显示,自动适配折叠屏等设备。

6. ViewModel 集成与结果传递

通过 rememberViewModelStoreNavEntryDecorator() 将 ViewModel 限定到 NavEntry,需配合 rememberSaveableStateHolderNavEntryDecorator()。ResultEventBus(Navigation 3 1.2.0+)使用键标识和路由结果,接收方通过 conflateAsState() 观察最新结果。官方 nav3-recipes 仓库提供了多步导航、状态保留、结果传递等常见场景的实现参考。

下一课预告:第11课将深入依赖注入------Koin 在 CMP 中的完整实践,包括 Koin 的跨平台优势、模块定义与作用域、Koin 注解与编译器插件、ViewModel 注入与构造参数传递、平台特定依赖的 expect/actual 注入策略,以及测试环境中的 Koin 替换。

相关推荐
FungLeo8 小时前
成为全栈·Flutter App 篇·第四批复盘:复用了什么,移动端又新增了什么
flutter·移动开发·跨端开发·成为全栈·api契约·全栈复盘
传奇开心果编程1 天前
【Compose Multiplatform跨端声明式UI实用技术】第7课:列表与惰性布局
移动开发·列表·编程语言理论·开发范式·编程框架·声明式ui·惰性布局
传奇开心果编程2 天前
【Compose Multiplatform跨端声明式UI实用技术】第6课:常用组件与 Material 3 设计系统
移动开发·编程语言理论·开发范式·编程框架·声明式ui·常用组件·material 3 设计系统
FungLeo2 天前
成为全栈·Flutter App 篇·Markdown 编辑器:编辑、预览与离开保护
flutter·移动开发·markdown编辑器·表单设计·成为全栈·草稿保存
传奇开心果编程2 天前
【Compose Multiplatform跨端声明式UI实用技术】第3课:Compose Hot Reload 与开发效率工具链
移动开发·编程语言理论·热重载·开发范式·编程框架·声明式ui·效率工具链
FungLeo3 天前
成为全栈·Flutter App 篇·内置 WebView:网页历史、App 返回与外链安全
flutter·网络安全·移动开发·webview·路由管理·成为全栈
传奇开心果编程3 天前
【Compose Multiplatform跨端声明式UI实用技术】第4课:Composable 函数与声明式 UI 思维
移动开发·编程语言理论·开发范式·编程框架·函数式思维·声明式ui·声明式ui思维
传奇开心果编程3 天前
【Compose Multiplatform跨端声明式UI实用技术】第1课:CMP 全景解读——为什么选择声明式跨平台UI
移动开发·编程语言理论·开发范式·编程框架·声明式ui·跨平台声明式ui·逻辑共享与ui共享
FungLeo4 天前
成为全栈·Flutter App 篇·分类、标签与搜索:移动端内容发现
flutter·移动开发·标签·搜索·分类管理·成为全栈