Fragment Navigation
Fragment Navigation:保留第四版正文机制,以所有者—状态—结果合同、事件轨迹和章专属故障完成可重放验收。
学习目标
- 能沿“Fragment Navigation”的用户事件解释Android组件、状态所有者、线程与销毁边界。
- 能围绕“掌握 Jetpack Navigation 组件的导航图、Safe Args 类型安全传参、以及底部导航栏和返回栈管理。”改出一个可运行结果,并用前后状态而非组件数量验收。
- 能在旋转、进程重建、拒权、离线或无效输入中选择适用反例,定位首个状态分叉。
- 能用构建指纹、用户操作、状态快照、原始日志和行为断言独立重放结论,并标明第四版机制与现代targetSdk政策的边界。
为什么导航需要专门的组件?
传统的 Fragment 导航是这样的:你手动 supportFragmentManager.beginTransaction().replace(...).addToBackStack(...).commit()。看起来还算清晰——直到你需要:
- 从 "列表页→详情页→编辑页" 三层跳转,按返回键要跳过编辑页直接回列表页
- 底部导航栏四个 Tab,每个 Tab 有自己独立的返回栈
- 从通知直接跳进"订单详情"这个深层页面(深链)
手动写这些逻辑时,不同开发者的回退栈行为千奇百怪——点两次返回键就崩溃是家常便饭。
Jetpack Navigation 组件把"从哪里来、到哪里去、怎么回"这件事统一管理。你画一张导航图(类似地铁线路图——每个站点是目的地,箭头是跳转动作),组件自动帮你处理 Fragment 切换和返回栈。Safe Args 插件还让你在页面间传参数时享受编译期类型检查——再也不会因为 key 拼错而运行时崩溃。
Navigation 是怎么工作的
三个核心角色
Navigation 组件由三个角色协同工作:
- ↡Navigation Graph 是一个 XML 文件,里面定义了一张「导航地图」——有哪些目的地(Fragment/Activity)、目的地之间怎么跳转(动作)、每个目的地需要什么参数。类比地铁线路图——站就是目的地,箭头就是怎么走。 —— 一张"地铁线路图"。用 XML 描述:有哪些"站"(destination)、站之间怎么跳(action)、每个站需要什么"票"(argument)
- ↡NavHostFragment 是 Activity 布局里一个特殊的 Fragment 容器——它有「换站」的能力。你把 Navigation Graph 交给它,它就按图帮你切换 Fragment。 —— 地铁的"站台"。它是 Activity 布局里一个特殊的 Fragment 容器,负责按导航图切换 Fragment
-
↡NavController 是导航的执行者。你调 navigate() 让它执行跳转,popBackStack() 让它返回上一站。同一个 Activity 只有一个 NavController。
—— 地铁的"调度员"。你告诉它"去这一站"(
navigate()),它实际执行 Fragment 事务并管理返回栈
第 1 / 5 步 · ① App 启动停在 startDestination = ListFragment(当前目的地),返回栈深 1
点击播放,看 navigate(action) 如何沿 action 边前进入栈、按返回键 popBackStack 如何沿来路后退出栈;当前目的地高亮始终对应屏幕上正显示的 Fragment,右下返回栈深度跟着步进。可暂停、单步、拖进度逐帧观察。
Safe Args:让传参在编译期就安全
传统传参方式:arguments?.putString("KEY", value)——如果接收方写成 arguments?.getString("KAY")(拼错了一个字母),编译没报错,运行时才会炸。Safe Args 是编译期生成的代码——你在导航图里声明参数,插件自动生成 Directions 类,调用时 IDE 有补全、编译期有检查。
猜一猜:如果你用
navigate()连续跳转 5 个 Fragment,按返回键会怎样?需要按几下才能回到第一个?
① 创建 Navigation Graph XML
在 res/navigation/ 目录下创建如 nav_graph.xml,声明 <navigation> 根元素及 app:startDestination(第一个显示的页面)。用 <fragment> 标签定义每个目的地(android:name 指向 Fragment 全限定类名),在目的地内部用 <argument> 声明需要的参数及其类型。
代码逐段拆解
第一步:创建 Navigation Graph
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_graph"
app:startDestination="@id/crimeListFragment">
<fragment
android:id="@+id/crimeListFragment"
android:name="com.example.criminalintent.CrimeListFragment"
android:label="案件列表"
tools:layout="@layout/fragment_crime_list">
<action
android:id="@+id/showCrimeDetail"
app:destination="@id/crimeDetailFragment" />
</fragment>
<fragment
android:id="@+id/crimeDetailFragment"
android:name="com.example.criminalintent.CrimeDetailFragment"
android:label="案件详情"
tools:layout="@layout/fragment_crime_detail">
<argument
android:name="crimeId"
app:argType="java.util.UUID" />
</fragment>
</navigation>app:startDestination 指定第一个显示的页面——相当于地铁图的"起点站"。
第二步:在 Activity 布局中嵌入 NavHostFragment
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph" />app:defaultNavHost="true" 表示这个 NavHostFragment 会拦截系统的返回键——让它走 Navigation 的返回栈,不会直接退到桌面。
第三步:用 Safe Args 导航
Safe Args 插件会在编译期自动生成 CrimeListFragmentDirections 类:
class CrimeListFragment : Fragment(R.layout.fragment_crime_list) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 点击条目 → 跳转详情页并传递 crimeId
val action = CrimeListFragmentDirections.showCrimeDetail(crimeId)
findNavController().navigate(action)
}
}接收方用 navArgs() 安全取出参数:
class CrimeDetailFragment : Fragment(R.layout.fragment_crime_detail) {
private val args: CrimeDetailFragmentArgs by navArgs()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val crimeId = args.crimeId // 编译期安全,IDE 自动补全
}
}第四步:管理返回栈
<action
android:id="@+id/showCrimeDetail"
app:destination="@id/crimeDetailFragment"
app:popUpTo="@id/crimeListFragment"
app:popUpToInclusive="false" />popUpTo="@id/crimeListFragment" 的意思是:导航到详情页之前,把返回栈里从当前页到 crimeListFragment 之间所有页面都弹出。popUpToInclusive="false" 表示 crimeListFragment 本身保留(不弹)。当你从编辑页点保存回到列表页时,返回栈就干净了——按返回键不会回到编辑页。
第五步:底部导航栏集成
class MainActivity : AppCompatActivity() {
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val navHostFragment = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController
val bottomNav = findViewById<BottomNavigationView>(R.id.bottom_nav)
bottomNav.setupWithNavController(navController)
}
}setupWithNavController 自动处理 Tab 点击 → 导航 + 选中状态同步 + 返回栈管理——一行代码搞定底部导航所有逻辑。
容易踩的坑
小结
- Navigation Graph 用 XML 描述"目的地地图"——哪些页面、怎么跳、带什么参数
- NavHostFragment 是容器,NavController 是调度员——navigate() 跳转,popBackStack() 返回
- Safe Args 把参数从运行时字符串变成编译期生成的强类型 Directions 类
popUpTo清理返回栈避免重复堆积;defaultNavHost="true"拦截系统返回键
练习
问题 1:“第12章 Fragment Navigation”覆盖哪些正式节点和项目主线?
问题 2:怎样建立本页最小可执行实验?
问题 3:为什么只在正常点击路径运行不能证明完成?
问题 4:怎样设计能推翻当前实现的反例?
问题 5:从第4版语境迁移到现代目标SDK时如何控制变量?
问题 6:本页达到独立交接标准需要什么?
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- Safe Args
Navigation 的编译期参数安全插件。你在导航图里声明参数类型,插件自动生成 Directions 类——类型检查从运行时提前到编译期,参数 key 永远不会拼错。
“Fragment Navigation”不使用未获授权的纸书正文;InformIT出版信息与授权电子版完整目录只用于确认第四版32章、269个正式目录节点和时代语境,第四版官方勘误用于识别工具链变更。下列中文解释、图示、交互、代码与练习均为独立教学重写,平台行为再以Android Developers的一手文档复核。
为什么“Fragment Navigation”必须回到可观察状态
“Fragment Navigation”的学习结果不是记住类名,而是能预测“掌握 Jetpack Navigation 组件的导航图、Safe Args 类型安全传参、以及底部导航栏和返回栈管理。”在一次输入、一次重建和一次失败中的不同状态,并指出哪条Android合同产生差异。
第四版机制逐项深读
12. Fragment Navigation
在“Fragment Navigation”中,分析“12. Fragment Navigation”要标出事务提交、参数写入、目标结果和返回栈变化,避免用活动中的临时字段传递可恢复状态。
Single Activity: Fragment Boss
在“Fragment Navigation”中,“Single Activity: Fragment Boss”由FragmentManager保存结构和返回栈,而Fragment实例与其View拥有不同生命周期;视图绑定必须在onDestroyView前释放。
Fragment Arguments
在“Fragment Navigation”中,验证“Fragment Arguments”在旋转、后台恢复和快速连点下比较fragment列表、back stack与目标数据,只出现一次导航副作用。
Using LiveData Transformations
在“Fragment Navigation”中,验证“Using LiveData Transformations”从空库、重复键、并发写与旧schema升级四条路径核对行数、约束、通知和回滚。
Updating the Database
在“Fragment Navigation”中,验证“Updating the Database”从空库、重复键、并发写与旧schema升级四条路径核对行数、约束、通知和回滚。
For the More Curious: Why Use Fragment Arguments?
在“Fragment Navigation”中,分析“For the More Curious: Why Use Fragment Arguments?”要标出事务提交、参数写入、目标结果和返回栈变化,避免用活动中的临时字段传递可恢复状态。
For the More Curious: Navigation Architecture Component Library
在“Fragment Navigation”中,验证“For the More Curious: Navigation Architecture Component Library”用同一事件序列比较旋转前后状态和副作用次数;界面相同但重复写入仍是不正确。
Challenge: Efficient RecyclerView Reloading
在“Fragment Navigation”中,“Challenge: Efficient RecyclerView Reloading”的性能由帧时间、绑定分配与diff范围共同决定,不以单次滑动的主观流畅度验收。
“Fragment Navigation”验收回顾
“Fragment Navigation”只有在构建指纹、用户操作、状态快照、原始日志和行为断言能够从相同基线再次得到相同断言时才通过;第四版机制与现代平台政策分别记录,不用新API名称掩盖旧行为。
章专属可重放状态实验
先预测“列表点击、详情编辑、返回、旋转与进程重建”发生后,FragmentManager、导航容器与 CrimeRepository应怎样改变目的地、参数、返回栈、选中 Crime ID 和更新结果;再操作三个实验。第四版示例与当前 Android 政策分别记录,实验不把新 API 名称倒填为原书内容。
实验一:所有者—状态—结果合同
选择任一正式目录节点和正常/边界场景,检查它是否真的进入本章状态合同。目录标题只有同时出现在解释、可视状态和交付证据中才算覆盖。
Owner · state · observable result
Fragment Navigation:状态合同
用 Fragment arguments、返回栈和数据库 ID 导航 CriminalIntent
验证场景
第四版正式目录节点
fragment-navigation · 正常任务
12. Fragment Navigation:固定 SDK、设备配置和初始状态,触发“列表点击、详情编辑、返回、旋转与进程重建”
冻结入口:12. Fragment Navigation
记录FragmentManager、导航容器与 CrimeRepository的初始目的地、参数、返回栈、选中 Crime ID 和更新结果
观察:事务、back stack、argument、实例 ID 和数据库更新断言中的“12. Fragment Navigation”轨迹
预期:由FragmentManager、导航容器与 CrimeRepository提交目的地、参数、返回栈、选中 Crime ID 和更新结果,并持续满足“目的地只接收稳定 ID,并从事实源恢复详情而非携带整份可变对象”
实验二:事件与生命周期轨迹
沿五次转换逐步执行“列表点击、详情编辑、返回、旋转与进程重建”。每一步只允许FragmentManager、导航容器与 CrimeRepository按职责提交状态,并持续核对“目的地只接收稳定 ID,并从事实源恢复详情而非携带整份可变对象”。
Deterministic event replay
Fragment Navigation:事件轨迹
不变量:目的地只接收稳定 ID,并从事实源恢复详情而非携带整份可变对象
交付证据:事务、back stack、argument、实例 ID 和数据库更新断言
实验三:章专属反例与同输入恢复
注入“状态保存后继续提交事务,旋转时抛出 state loss 或显示重复 Fragment”,保存第一个偏离点;撤销后以完全相同的 SDK、设备状态和用户事件重放。只有事务、back stack、argument、实例 ID 和数据库更新断言一起恢复才算修复。
Fault · cancel · restore
Fragment Navigation:反例与恢复
故障:状态保存后继续提交事务,旋转时抛出 state loss 或显示重复 Fragment
第 1 次使用相同 SDK、设备配置、初始状态与用户事件
保持正常输入不变,仅注入“状态保存后继续提交事务,旋转时抛出 state loss 或显示重复 Fragment”
目的地只接收稳定 ID,并从事实源恢复详情而非携带整份可变对象
事务、back stack、argument、实例 ID 和数据库更新断言