一、ViewModel
ViewModel 的核心原理是通过一个独立于UI(Activity/Fragment)生命周期的容器,来存储和管理界面所需的数据。其设计的精髓在于数据与UI生命周期的分离,由 ViewModelStore 负责在屏幕旋转等配置变更时保留数据。
下面这张流程图清晰地展示了 ViewModel 的创建、存储和生命周期管理的核心机制:
flowchart TD
A[Activity/Fragment 首次创建] --> B[初始化 ViewModelStore]
B --> C[通过 ViewModelProvider.get 请求 ViewModel]
C --> D{"ViewModelStore 中<br>是否存在实例?"}
D -- 是 --> E[返回已存在的实例]
D -- 否 --> F[通过 Factory 创建新实例]
F --> G[存入 ViewModelStore 并返回]
E --> H[屏幕旋转等配置变更]
G --> H
subgraph Z[配置变更期间]
H --> I["Activity 被销毁,但<br>ViewModelStore 被系统保留"]
I --> J[新的 Activity 实例被创建<br>并接收到保留的 ViewModelStore]
end
J --> C
K["Activity 真正销毁<br>(如返回或退出)"] --> L[ViewModelStore.clear<br>触发所有 ViewModel.onCleared]
1. 核心原理深度解析
理解上图中的几个核心组件是关键:
- ViewModelStore: 如图中所示,它是一个简单的键值对容器(内部是
HashMap),是 ViewModel 实例的“储藏室”。它的特殊之处在于,当 Activity 因配置变更被销毁重建时,它会被系统通过onRetainNonConfigurationInstance()机制临时保留下来,并传递给新的 Activity 实例,从而实现 ViewModel 的“存活”。 - ViewModelProvider: 这是获取 ViewModel 的统一入口。它遵循 “get-or-create” 模式:当使用
ViewModelProvider(owner).get(MyViewModel::class.java)时,它会先以类的规范名称为 Key,去属于owner(如 Activity)的ViewModelStore中查找;如果找到就直接返回,如果没找到则通过Factory创建并存入ViewModelStore。 - viewModelScope: 这是 ViewModel 的一个 Kotlin 扩展属性,它绑定了一个与 ViewModel 生命周期一致的协程作用域。当 ViewModel 被清除时(
onCleared()),此作用域内的所有协程会自动取消,有效避免了内存泄漏和后台任务积压。
2. ViewModel 在架构中的角色与协作
ViewModel 是 MVVM(Model-View-ViewModel) 架构模式的核心组件,负责连接 View(UI) 和 Model(数据层/业务逻辑) 。
-
职责划分:
- View (UI控制器) : 仅负责显示数据和接收用户输入,不处理业务逻辑。
- ViewModel: 负责为 View 准备和暴露 UI 所需的数据状态(State),并处理来自 View 的用户交互事件,向 Model 层发起请求。
- Model: 负责数据获取和持久化(如数据库、网络)。
-
- 状态下行:Model 提供数据 -> ViewModel 加工为 UI 状态 -> View 观察并渲染。
- 事件上行:View 触发用户事件 -> ViewModel 处理 -> 必要时通知 Model 更新数据。
-
与 LiveData/StateFlow 的协作:
ViewModel 内部通常使用MutableLiveData或MutableStateFlow来持有可变的 UI 状态,但对外仅暴露其只读版本(LiveData或StateFlow)。这样既保证了数据可被观察,又封装了修改权,确保数据变化只能通过 ViewModel 的特定方法进行,符合单向数据流。
3. 要点详解
3.1 基础与原理类
-
ViewModel 是什么?解决了什么问题?
ViewModel 是一个生命周期感知的组件,用于以结构化的方式存储和管理与 UI 相关的数据。它核心解决了屏幕旋转等配置变更导致的数据丢失问题,同时帮助实现了 UI 控制器(Activity/Fragment)与业务逻辑的关注点分离。
-
ViewModel 如何在屏幕旋转后存活?
其核心机制是
ViewModelStore。配置变更时,Activity 被销毁,但ViewModelStore会被系统通过onRetainNonConfigurationInstance()机制临时保留下来。新的 Activity 实例创建后,会接收到同一个ViewModelStore,从而获取到里面保存的 ViewModel 实例。 -
ViewModel 和
onSaveInstanceState()的区别?onSaveInstanceState(): 用于保存少量、可序列化的 UI 状态(如文本框内容),以应对系统内存不足、进程被杀后恢复的场景。数据会序列化到磁盘。- ViewModel: 用于存储和管理相对较大、不可序列化的 UI 数据(如用户列表、网络请求结果),仅在同一进程内的配置变更(如旋转)时存活。数据保存在内存中。
3.2 使用与进阶类
-
如何正确创建 ViewModel?为何不能直接
new?必须通过
ViewModelProvider来获取实例。如果直接new,创建的 ViewModel 无法与当前生命周期的所有者(如 Activity)关联,既无法在配置变更时保留,其viewModelScope也无法正确销毁,会导致内存泄漏。 -
ViewModel 可以持有 Context 吗?
避免持有 Activity 等生命周期短的 Context 引用,以防内存泄漏。如果确实需要应用上下文,可以使用
AndroidViewModel(它是ViewModel的子类,内部持有ApplicationContext)。 -
如何在 Fragment 间共享数据?
让 Fragments 共享它们宿主 Activity 作用域的 ViewModel。使用
by activityViewModels()委托来获取同一个实例,从而实现数据共享和通信。 -
StateFlow 与 LiveData 如何选择?
特性 LiveData StateFlow 生命周期感知 原生支持,自动管理 需要配合 repeatOnLifecycle等手动控制数据重放 默认仅最新值 始终重放最新值给新订阅者 协程支持 有限 原生支持,基于协程 适用场景 简单 UI 状态、Java 项目 复杂数据流、纯 Kotlin 项目、需要更多操作符 在新项目中,如果全面使用 Kotlin 协程,更推荐使用 StateFlow,因为它提供更严格的线程控制和丰富的变换操作。如果项目简单或仍需支持 Java,LiveData 仍是好选择。
-
如何防止
viewModelScope造成的内存泄漏?viewModelScope的设计已经考虑了生命周期,它会在 ViewModel 的onCleared()时自动取消。开发者需要注意:- 不要在
viewModelScope中启动无限循环的协程。 - 在协程内部进行耗时操作时,应使用
try/catch或CoroutineExceptionHandler妥善处理异常,避免因未捕获异常导致作用域取消链断裂。 - 对于需要超出 ViewModel 生命周期的任务(如上传日志),应使用
Application作用域的协程或其他机制。
- 不要在
二、SavedStateHandle
当应用进程被系统杀死(Process Death)后,SavedStateHandle 是确保关键 UI 状态能够恢复的关键组件。它与 ViewModel 集成,提供了一种比 onSaveInstanceState 更优雅、更内聚的数据保存方案。
1. 进程死亡 vs. 配置变更
首先必须明确这两者的根本区别,这也是理解 SavedStateHandle 必要性的前提:
| 场景 | 触发条件 | 数据存储位置 | ViewModel 是否存活 | 恢复机制 |
|---|---|---|---|---|
| 配置变更 | 屏幕旋转、语言切换、深色模式切换等。 | 内存 中(通过ViewModelStore保留)。 | 是,ViewModel实例被保留。 | 自动,系统重建Activity并重新关联同一个ViewModel。 |
| 进程死亡 | 系统资源不足、用户长时间切到后台、应用发生崩溃。 | 磁盘 上(由系统将Bundle写入磁盘)。 | 否,整个应用进程被销毁,内存清空。 | 手动,系统重建Activity和ViewModel,开发者需从SavedStateHandle恢复数据。 |
简单来说,SavedStateHandle 就是 ViewModel 为应对进程死亡场景而配备的“逃生背包”。它允许你将必要的状态(如当前列表的滚动位置、临时填写的表单内容)打包存盘,在用户回来时恢复现场。
2. SavedStateHandle 核心原理与工作流程
其核心原理是:将数据保存到系统管理的 Bundle 中,该 Bundle 最终会通过 Activity 的 onSaveInstanceState() 机制写入磁盘,并在重建时恢复。
它的工作流程可以通过下图完整展示:
flowchart TD
A["用户在界面操作<br>(如编辑、滚动)"] --> B["ViewModel 通过 SavedStateHandle<br>更新状态 (如 set)"]
B --> C[SusavedStateHandle 将数据存入<br>内部 Bundle]
subgraph D [进程死亡与恢复周期]
D1[系统因内存不足<br>杀死应用进程] --> D2[触发 Activity.onSaveInstanceState<br>SavedStateRegistry 将 Bundle 写入磁盘]
D2 --> D3["用户返回,系统重建进程"]
D3 --> D4[创建新 ViewModel 实例<br>并注入恢复的 Bundle]
end
C -.->|"“自动关联”"| D2
D4 --> E[新 ViewModel 的 SavedStateHandle<br>包含恢复的数据]
E --> F[UI 通过 LiveData/StateFlow<br>观察到恢复的状态并刷新]
关键实现细节:
- 自动序列化:
SavedStateHandle内部使用BaseSavedStateRegistry,能自动处理可序列化(Serializable/Parcelable)的数据类型。对于复杂对象,需要手动转换(如转为 JSON 字符串)。 - 与 ViewModel 集成:当通过
ViewModelProvider创建带有SavedStateHandle参数的 ViewModel 时,系统会自动将恢复的 Bundle 注入。 - 自定义保存逻辑:通过
SavedStateRegistry的registerSavedStateProvider方法,可以在保存时刻动态生成要保存的状态,适用于那些无法实时同步到SavedStateHandle的复杂数据。
3. 与传统 onSaveInstanceState 的对比
SavedStateHandle 在架构上更加先进:
| 方面 | 传统 onSaveInstanceState | ViewModel + SavedStateHandle |
|---|---|---|
| 保存位置 | Activity/Fragment 中。 | ViewModel 中,与数据逻辑放在一起。 |
| 职责分离 | 违反单一职责,UI控制器需要关心数据保存。 | 符合关注点分离,ViewModel负责数据持久化。 |
| 数据访问 | 需要在onCreate中读取Bundle并手动分发。 | 可观察,通过getLiveData()直接暴露给UI观察。 |
| 类型安全 | 从Bundle存取数据需要键名,容易出错。 | 通过键值对存取,并与LiveData/StateFlow结合,类型更安全。 |
| 测试便利性 | 难以模拟系统保存和恢复的Bundle。 | ViewModel可以独立测试,SavedStateHandle可轻松模拟。 |
4. 在 ViewModel 中使用 SavedStateHandle
在 ViewModel 的构造函数中添加 SavedStateHandle 参数即可使用:
// 1. 在 ViewModel 中接收 SavedStateHandle
class MyViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
// 2. 定义状态键(推荐使用常量)
companion object {
private const val SEARCH_QUERY_KEY = "search_query"
private const val SELECTED_ITEM_ID_KEY = "selected_item_id"
}
// 3. 方式一:获取一个与状态绑定的 LiveData
// 初始值来自 savedStateHandle(如果进程死亡后恢复,则有值;否则为默认值)
val searchQuery: LiveData<String> =
savedStateHandle.getLiveData(SEARCH_QUERY_KEY, "")
fun updateSearchQuery(query: String) {
// 更新 LiveData 的值,同时会自动保存到 SavedStateHandle
savedStateHandle[SEARCH_QUERY_KEY] = query
}
// 4. 方式二:获取一个与状态绑定的 StateFlow (推荐用于 Kotlin 项目)
val selectedItemId: StateFlow<Int?> =
savedStateHandle.getStateFlow(SELECTED_ITEM_ID_KEY, null)
fun selectItem(id: Int) {
savedStateHandle[SELECTED_ITEM_ID_KEY] = id
}
// 5. 方式三:直接存取(适用于不需要观察的简单数据)
fun saveTempData(value: String) {
savedStateHandle["temp_key"] = value
}
fun getTempData(): String? = savedStateHandle.get<String>("temp_key")
// 6. 移除不需要持久化的数据
fun clearTempData() {
savedStateHandle.remove("temp_key")
}
}
在 Activity/Fragment 中,你无需做任何特殊处理,像普通 ViewModel 一样获取即可:
// Activity/Fragment 中,像往常一样获取 ViewModel
// 系统会自动处理 SavedStateHandle 的注入
private val viewModel: MyViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 直接观察来自 SavedStateHandle 的 LiveData/StateFlow
viewModel.searchQuery.observe(viewLifecycleOwner) { query ->
// UI 会自动接收到恢复的搜索词
editText.setText(query)
}
}
5. 最佳实践与要点详解
5.1 使用原则
- 存小存精:只保存最小必要的 UI 状态(如 ID、关键字、索引),而非完整数据对象(如整个用户列表)。完整数据应从 Model 层(数据库、网络)重新加载。
- 数据类型:优先保存
Parcelable,Serializable, 基础类型(Int,String)。复杂对象需手动序列化(如使用 Gson 转为 JSON 字符串)。 - 组合使用:将
SavedStateHandle用于“进程死亡恢复”,将ViewModel的常规属性用于“配置变更保留”,将 Room 数据库用于“永久存储”。
5.2 要点详解
-
SavedStateHandle与onSaveInstanceState有什么区别和联系?联系:底层都通过 Activity 的
onSaveInstanceState机制将 Bundle 写入磁盘。
区别:SavedStateHandle将保存逻辑上移到了 ViewModel,使数据保存成为业务逻辑的一部分,而不是视图控制器的职责,更符合架构原则。 -
ViewModel 已经能在旋转后保存数据,为什么还需要
SavedStateHandle?这是为了应对 “进程死亡” 这一更彻底的数据销毁场景。ViewModel 的存活依赖内存,而进程死亡后内存清空。
SavedStateHandle通过磁盘备份,提供了最后一道保障。 -
什么数据应该用
SavedStateHandle保存?应保存临时、易失、但直接影响用户体验的状态。例如:
- 用户正在填写的表单内容(未提交)。
- 列表的滚动位置。
- 当前选中的标签页或筛选条件。
- 导航栈中的当前目的地(如结合 Navigation Component)。
-
如何测试包含
SavedStateHandle的 ViewModel?可以使用
SavedStateHandle.createHandle()创建测试用的句柄:@Test fun testSavedStateHandle() { // 1. 创建模拟的 SavedStateHandle 并存入初始状态 val savedStateHandle = SavedStateHandle.createHandle( null, // 没有恢复的 Bundle null ) savedStateHandle["key"] = "initial_value" // 2. 传入 ViewModel 进行测试 val viewModel = MyViewModel(savedStateHandle) // 3. 断言行为 assertEquals("initial_value", viewModel.getSomeData()) }
总结来说,SavedStateHandle 是构建健壮 Android 应用的重要工具,它巧妙地将进程死亡恢复这一系统级机制,封装成了 ViewModel 内部一个易于管理和测试的组件。正确使用它,能让你的应用在极端情况下依然为用户提供连贯的体验。