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 组件由三个角色协同工作:

  • —— 一张"地铁线路图"。用 XML 描述:有哪些"站"(destination)、站之间怎么跳(action)、每个站需要什么"票"(argument)
  • —— 地铁的"站台"。它是 Activity 布局里一个特殊的 Fragment 容器,负责按导航图切换 Fragment
  • —— 地铁的"调度员"。你告诉它"去这一站"(navigate()),它实际执行 Fragment 事务并管理返回栈
可交互
Navigation Graph:目的地 = 站,action = 跳转箭头action_list_to_detailaction_detail_to_settings★ startDestinationListFragment列表页DetailFragment详情页SettingsFragment设置页返回栈深度

第 1 / 5 步 · ① App 启动停在 startDestination = ListFragment(当前目的地),返回栈深 1

点击播放,看 navigate(action) 如何沿 action 边前进入栈、按返回键 popBackStack 如何沿来路后退出栈;当前目的地高亮始终对应屏幕上正显示的 Fragment,右下返回栈深度跟着步进。可暂停、单步、拖进度逐帧观察。

Navigation 导航图:startDestination 是起点站,navigate(action) 沿 action 边前进、上一站入返回栈, 按系统返回键 = popBackStack 沿来路弹出栈顶回到上一站。当前目的地高亮即屏幕上正显示的 Fragment。

Safe Args:让传参在编译期就安全

传统传参方式:arguments?.putString("KEY", value)——如果接收方写成 arguments?.getString("KAY")(拼错了一个字母),编译没报错,运行时才会炸。Safe Args 是编译期生成的代码——你在导航图里声明参数,插件自动生成 Directions 类,调用时 IDE 有补全、编译期有检查。

猜一猜:如果你用 navigate() 连续跳转 5 个 Fragment,按返回键会怎样?需要按几下才能回到第一个?

分步1 / 5

① 创建 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:本页达到独立交接标准需要什么?

名词解释

名词解释

本章出现的专业名词,用大白话再讲一遍。

Navigation Graph

一张描述"页面地图"的 XML 文件。声明所有目的地、它们之间的跳转动作、以及每个目的地需要的参数。可视化为地铁线路图——站是 Fragment,箭头是导航动作。

NavHostFragment

Activity 里的 Fragment 容器——但不是普通的容器。它持有 Navigation Graph,按图自动切换 Fragment。是整条地铁线的"站台"。

NavController

导航的执行引擎。你告诉它"去某站",它处理 Fragment 事务和返回栈。通过 findNavController() 拿到。

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名称掩盖旧行为。

← 上一页:数据库与Room库 · 下一页:对话框 →

章专属可重放状态实验

先预测“列表点击、详情编辑、返回、旋转与进程重建”发生后,FragmentManager、导航容器与 CrimeRepository应怎样改变目的地、参数、返回栈、选中 Crime ID 和更新结果;再操作三个实验。第四版示例与当前 Android 政策分别记录,实验不把新 API 名称倒填为原书内容。

实验一:所有者—状态—结果合同

选择任一正式目录节点和正常/边界场景,检查它是否真的进入本章状态合同。目录标题只有同时出现在解释、可视状态和交付证据中才算覆盖。

Owner · state · observable result

Fragment Navigation:状态合同

用 Fragment arguments、返回栈和数据库 ID 导航 CriminalIntent

验证场景

第四版正式目录节点

fragment-navigation · 正常任务

12. Fragment Navigation固定 SDK、设备配置和初始状态,触发“列表点击、详情编辑、返回、旋转与进程重建”

状态所有者FragmentManager、导航容器与 CrimeRepository
受控状态目的地、参数、返回栈、选中 Crime ID 和更新结果
触发事件列表点击、详情编辑、返回、旋转与进程重建

冻结入口: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:事件轨迹

选择一次状态转换1 / 5

不变量:目的地只接收稳定 ID,并从事实源恢复详情而非携带整份可变对象

交付证据:事务、back stack、argument、实例 ID 和数据库更新断言

实验三:章专属反例与同输入恢复

注入“状态保存后继续提交事务,旋转时抛出 state loss 或显示重复 Fragment”,保存第一个偏离点;撤销后以完全相同的 SDK、设备状态和用户事件重放。只有事务、back stack、argument、实例 ID 和数据库更新断言一起恢复才算修复。

Fault · cancel · restore

Fragment Navigation:反例与恢复

故障:状态保存后继续提交事务,旋转时抛出 state loss 或显示重复 Fragment

1. 冻结输入一致

第 1 次使用相同 SDK、设备配置、初始状态与用户事件

2. 注入边界一致

保持正常输入不变,仅注入“状态保存后继续提交事务,旋转时抛出 state loss 或显示重复 Fragment”

3. 检查所有者一致

目的地只接收稳定 ID,并从事实源恢复详情而非携带整份可变对象

4. 核对结果一致

事务、back stack、argument、实例 ID 和数据库更新断言

讨论

评论区加载中…