4. SavedStateHandle 与状态持久化
大家都知道,Compose 是响应式的——状态一变,UI 自动更新。但有个老问题始终绕不开:屏幕旋转、进程重建甚至用户切后台被杀掉,状态怎么恢复?我早期做传统 View 系统时被这个问题坑过好多次,后来在 Compose 里第一次遇到类似的场景,下意识就想去找 saveInstanceState。嗯,Android 官方其实已经给我们提供了更优雅的方案——SavedStateHandle。
4.1 为什么需要 SavedStateHandle?
说白了,ViewModel 本身只有在对应的 Activity/Fragment 被销毁后才跟着销毁。但如果是进程被系统杀死,或者屏幕旋转这类配置变更,ViewModel 虽然还在(因为默认开启了 onRetainNonConfigurationInstance),可它里面引用的对象内存会被清空——尤其是那些没有持久化的数据。我有个项目里用户填了一整个表单,旋转屏幕后所有输入全没了,产品经理当场就炸了。
官方的解决方案就是:SavedStateHandle 配合 SavedStateRegistry,自动把你的数据存进 Bundle,恢复时再取出来。对开发者来说,你只需要声明好哪些数据要保存,剩下的事情框架帮你搞定。
4.2 SavedStateHandle 怎么用?
使用超简单。在你的 ViewModel 的构造函数里接收一个 SavedStateHandle 参数,然后像操作 Map 一样存取值。Type-safe 版本还可以用 getStateFlow() 或 getLiveData(),这样 Compose 可以直接观察它。
class FormViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
val userName: StateFlow<String> = savedStateHandle
.getStateFlow("userName", "")
val age: MutableStateFlow<Int> = savedStateHandle
.getStateFlow("age", 0)
fun updateName(name: String) {
savedStateHandle["userName"] = name
}
fun updateAge(age: Int) {
savedStateHandle["age"] = age
}
}
注意我用的是 getStateFlow,这样在 Composable 里直接 collectAsState() 就能拿到最新的值。你甚至可以把它当成一个持久化的 MutableState 来用——但底层它会自动序列化到 Bundle,进程杀掉也不怕。
4.3 跟 Compose 的 rememberSaveable 有啥区别?
这个问题我猜很多人会问。我简单对比一下:
| 特征 | rememberSaveable | SavedStateHandle |
|---|---|---|
| 作用范围 | 单个 Composable / 屏幕局部 | 整个 ViewModel(可跨多个 Composable) |
| 生命周期 | 跟随 Composable 所在的 SavedStateRegistry | 跟随 ViewModel(更持久,可跨 Activity 重建) |
| 数据共享 | 只能在同一个 Composable 树内 | 可以被多个 Fragment / Screen 共享 |
| 序列化能力 | 只能存 Bundle 支持的类型 | 同上,但可通过自定义 Parcelable 扩展 |
| Mock 回放场景 | 需手动保存到外部 | 天然支持,可直接注入测试数据 |
我自己定的原则是:如果状态只在单个屏幕内用,且不需要跨 Fragment 共享 → 用 rememberSaveable;如果状态是 ViewModel 的业务数据,或者要用来做 Mock 回放 → 用 SavedStateHandle。
4.4 SavedStateHandle 在 Mock 回放中的妙用
回到我们的课程主题。Mock 回放的核心是把录制时的状态快照存下来,然后在新的一次运行中「喂」给 UI。如果没有持久化,你录好的用例一旋转屏幕就没了,那还玩啥?
具体做法是这样:
- 录制时,把 ViewModel 里
savedStateHandle的当前所有键值对提取出来(可以通过反射或定义接口),存到本地文件。 - 回放时,创建 ViewModel 之前,先把文件中的快照传入一个
SavedStateHandle实例,再注入到 ViewModel 中。 - Compose UI 直接通过
getStateFlow拿到回放数据,无需任何额外处理。
我曾经在项目里用这种方法做到了「录制后旋转屏幕 → 回放依然正确显示相同界面」。核心代码大概这样:
// 回放时构建 SavedStateHandle
val mockSnapshot = loadSnapshotFromFile("case_001.json")
val mockHandle = SavedStateHandle(mockSnapshot.toMap())
val vm = ViewModelProvider(
viewModelStore,
ViewModelProvider.AndroidViewModelFactory(application)
).get(MyViewModel::class.java) // 注意:需要自定义 Factory 注入 mockHandle
savedStateHandle 的 key 写成了硬编码字符串,结果不同模块的 key 冲突了,导致回放时数据错乱。后来我强制在 ViewModel 里定义一个常量类,统一管理所有 key。切记:key 要有命名空间前缀或全局唯一。
4.5 生命周期与持久化原理
理解原理,用起来才安心。SavedStateHandle 的数据实际上走了这么一条路径:
看图就清楚了:当 Activity 重建时,SavedStateRegistry 会从 Bundle 中反序列化出之前保存的数据,然后注入到 ViewModel 的 SavedStateHandle 中。ViewModel 内部再通过 StateFlow 推送给 Compose UI。整个过程对开发者基本透明——你只要负责声明哪些 key 要存就行。
4.6 项目实战中的小技巧
- 不要把所有数据都往 SavedStateHandle 里塞,它会序列化到 Bundle,Bundle 大小是有限制的(官方建议 < 500KB)。大图片、长列表,老老实实用 Room 或 DataStore。
- Mock 回放时,用 SavedStateHandle 的构造函数直接注入预设数据,这样你连 ViewModel 的初始化代码都不用改。
- 可以结合 Hilt 的 @HiltViewModel,自动注入 SavedStateHandle。省掉手动创建 Factory 的麻烦。
- 注意 SavedStateHandle 不是跨进程的,进程杀掉后它依赖系统的 onSaveInstanceState 机制。如果你想保存更长时间(比如第二天打开 App),必须把它同步到持久化存储。