UI状态的保存与恢复

UI状态的保存与恢复:保留第四版正文机制,以所有者—状态—结果合同、事件轨迹和章专属故障完成可重放验收。

学习目标

  • 能沿“UI状态的保存与恢复”的用户事件解释Android组件、状态所有者、线程与销毁边界。
  • 能围绕“掌握 onSaveInstanceState 和 ViewModel——让用户在旋转屏幕或进程被杀死后回到原来的界面状态。”改出一个可运行结果,并用前后状态而非组件数量验收。
  • 能在旋转、进程重建、拒权、离线或无效输入中选择适用反例,定位首个状态分叉。
  • 能用实例ID、回调轨迹、旋转与进程恢复后的状态断言独立重放结论,并标明第四版机制与现代targetSdk政策的边界。

转一下屏幕,表单全没了

用户填到一半旋转手机——输入框被清空,体验等于白填。原因是 会重建 Activity,除非你把状态存到系统能带过去的地方。

两种主流方案:(轻量)和 (业务状态)。

onSaveInstanceState

override fun onSaveInstanceState(outState: Bundle) {
    super.onSaveInstanceState(outState)
    outState.putString("user_input", editText.text.toString())
}
 
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    savedInstanceState?.getString("user_input")?.let { editText.setText(it) }
}

Bundle 约 1MB 上限,只适合字符串、整型等可序列化小数据。

分步1 / 3

① 用户输入

用户在 EditText 中输入内容(如 "Hello"),数据存储在 Activity 实例的内存中——此时尚未持久化,旋转就会被清空。

ViewModel

class FormViewModel : ViewModel() {
    val userInput = MutableLiveData("")
}
 
class FormActivity : AppCompatActivity() {
    private val viewModel: FormViewModel by viewModels()
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        editText.setText(viewModel.userInput.value)
        editText.doAfterTextChanged { viewModel.userInput.value = it.toString() }
    }
}

旋转后仍是同一个 ViewModel 实例——不必手动 Bundle 也能保住数据。

可交互
时间 →Activity 实例(持有 UI 状态 / 成员变量)ViewModel:跨重建存活配置变更 / 旋转FormViewModel(同一实例贯穿旋转)Activity 实例 #1成员变量 / UI 状态Activity 实例 #2重建(onCreate)另:onSaveInstanceState 的 Bundle 也跨重建恢复轻量 UI 状态

第 1 / 5 步 · ① Activity 实例 #1 运行:持有 UI 状态与成员变量,ViewModel 已附着(生命线左端亮起)

点击播放,看一次旋转屏幕:Activity 在旋转点断裂、被换成新实例(成员变量随之清空),而 ViewModel 生命线连续贯穿不中断、数据保住。可暂停、单步、拖进度逐帧观察。

配置变更(旋转)下的对比:Activity 实例被销毁并重建(#1 灭、#2 起,成员变量丢失), 同一个 ViewModel 实例的生命线却连续贯穿旋转点不中断——这就是它能保住数据的原因。
onSaveInstanceStateViewModel
旋转支持支持
进程被杀支持(Bundle 持久化)默认丢失
数据量
复杂对象需序列化直接持有

小结

  • 旋转:ViewModel 或 onSaveInstanceState
  • 进程被杀:onSaveInstanceState / SavedStateHandle
  • ViewModel 不碰 View 层引用

练习

问题 1:“第4章 Persisting UI State”覆盖哪些正式节点和项目主线?

问题 2:怎样建立本页最小可执行实验?

问题 3:为什么只在正常点击路径运行不能证明完成?

问题 4:怎样设计能推翻当前实现的反例?

问题 5:从第4版语境迁移到现代目标SDK时如何控制变量?

问题 6:本页达到独立交接标准需要什么?

名词解释

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

配置变更

屏幕旋转、语言切换等导致 Activity 实例销毁并按新 Configuration 重建的过程。

onSaveInstanceState

系统在 Activity 可能被销毁前调用的回调,用于把 UI 状态写入 Bundle。

ViewModel

配置变更时存活的 UI 状态容器,由 ViewModelStore 管理,不持有 View 引用。

SavedStateHandle

ViewModel 构造参数,提供与 onSaveInstanceState 相同的 Bundle 能力,便于进程恢复。

“UI状态的保存与恢复”不使用未获授权的纸书正文;InformIT出版信息授权电子版完整目录只用于确认第四版32章、269个正式目录节点和时代语境,第四版官方勘误用于识别工具链变更。下列中文解释、图示、交互、代码与练习均为独立教学重写,平台行为再以Android Developers的一手文档复核。

为什么“UI状态的保存与恢复”必须回到可观察状态

“UI状态的保存与恢复”的学习结果不是记住类名,而是能预测“掌握 onSaveInstanceState 和 ViewModel——让用户在旋转屏幕或进程被杀死后回到原来的界面状态。”在一次输入、一次重建和一次失败中的不同状态,并指出哪条Android合同产生差异。

第四版机制逐项深读

4. Persisting UI State

在“UI状态的保存与恢复”中,“4. Persisting UI State”要求把临时UI状态、配置期状态与进程死亡后需恢复的最小事实分层;ViewModel跨配置但不跨进程,saved state必须小且可序列化。

Including the ViewModel Dependency

在“UI状态的保存与恢复”中,分析“Including the ViewModel Dependency”要画出事件进入、状态归约、数据源写入和UI重绘四条边,并指出配置变化时谁被重建。

Adding a ViewModel

在“UI状态的保存与恢复”中,“Adding a ViewModel”按事实、界面表示和用户事件拆责任;状态所有者不能持有已销毁View,界面也不能绕过边界直接改持久数据。

Saving Data Across Process Death

在“UI状态的保存与恢复”中,“Saving Data Across Process Death”要求把临时UI状态、配置期状态与进程死亡后需恢复的最小事实分层;ViewModel跨配置但不跨进程,saved state必须小且可序列化。

ViewModel vs Saved Instance State

在“UI状态的保存与恢复”中,验证“ViewModel vs Saved Instance State”用同一事件序列比较旋转前后状态和副作用次数;界面相同但重复写入仍是不正确。

For the More Curious: Jetpack, AndroidX, and Architecture Components

在“UI状态的保存与恢复”中,验证“For the More Curious: Jetpack, AndroidX, and Architecture Components”用同一事件序列比较旋转前后状态和副作用次数;界面相同但重复写入仍是不正确。

For the More Curious: Avoiding a Half-Baked Solution

在“UI状态的保存与恢复”中,“For the More Curious: Avoiding a Half-Baked Solution”用于推翻“掌握 onSaveInstanceState 和 ViewModel——让用户在旋转屏幕或进程被杀死后回到原来的界面状态。”的顺利路径:先写预期,再引入一个边界输入、重建或平台差异,并用状态、日志与用户结果解释首个分叉。

“UI状态的保存与恢复”验收回顾

“UI状态的保存与恢复”只有在实例ID、回调轨迹、旋转与进程恢复后的状态断言能够从相同基线再次得到相同断言时才通过;第四版机制与现代平台政策分别记录,不用新API名称掩盖旧行为。

← 上一页:activity的生命周期 · 下一页:Android应用的调试 →

章专属可重放状态实验

先预测“旋转、后台进程回收与冷启动恢复”发生后,QuizViewModel、SavedStateHandle 与持久仓库应怎样改变题号、作答记录、临时输入和可恢复事实;再操作三个实验。第四版示例与当前 Android 政策分别记录,实验不把新 API 名称倒填为原书内容。

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

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

Owner · state · observable result

UI状态的保存与恢复:状态合同

区分 View 临时状态、ViewModel 配置期状态、saved state 与持久事实

验证场景

第四版正式目录节点

ui-state-persistence · 正常任务

4. Persisting UI State固定 SDK、设备配置和初始状态,触发“旋转、后台进程回收与冷启动恢复”

状态所有者QuizViewModel、SavedStateHandle 与持久仓库
受控状态题号、作答记录、临时输入和可恢复事实
触发事件旋转、后台进程回收与冷启动恢复

冻结入口:4. Persisting UI State

记录QuizViewModel、SavedStateHandle 与持久仓库的初始题号、作答记录、临时输入和可恢复事实

观察:保存键、序列化值、进程 ID、恢复轨迹和行为断言中的“4. Persisting UI State”轨迹

预期:由QuizViewModel、SavedStateHandle 与持久仓库提交题号、作答记录、临时输入和可恢复事实,并持续满足“每类状态只由适合其寿命的所有者恢复且不重复提交副作用”

实验二:事件与生命周期轨迹

沿五次转换逐步执行“旋转、后台进程回收与冷启动恢复”。每一步只允许QuizViewModel、SavedStateHandle 与持久仓库按职责提交状态,并持续核对“每类状态只由适合其寿命的所有者恢复且不重复提交副作用”。

Deterministic event replay

UI状态的保存与恢复:事件轨迹

选择一次状态转换1 / 5

不变量:每类状态只由适合其寿命的所有者恢复且不重复提交副作用

交付证据:保存键、序列化值、进程 ID、恢复轨迹和行为断言

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

注入“只用 ViewModel 保存答案并假设它可以跨进程死亡”,保存第一个偏离点;撤销后以完全相同的 SDK、设备状态和用户事件重放。只有保存键、序列化值、进程 ID、恢复轨迹和行为断言一起恢复才算修复。

Fault · cancel · restore

UI状态的保存与恢复:反例与恢复

故障:只用 ViewModel 保存答案并假设它可以跨进程死亡

1. 冻结输入一致

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

2. 注入边界一致

保持正常输入不变,仅注入“只用 ViewModel 保存答案并假设它可以跨进程死亡”

3. 检查所有者一致

每类状态只由适合其寿命的所有者恢复且不重复提交副作用

4. 核对结果一致

保存键、序列化值、进程 ID、恢复轨迹和行为断言

讨论

评论区加载中…