4. rememberSaveable:保存状态穿越配置变更、Bundle支持与Parcelable、自定义Saver
在生产环境中,仅仅使用 remember 是不够的——当配置变更(如屏幕旋转)或进程重建时,remember 维护的状态会被直接丢弃,导致 UI 回到初始值,严重影响用户体验。rememberSaveable 是 Jetpack Compose 提供的解决方案,它本质上是一个自带 Bundle 持久化 能力的 remember,能够将状态优雅地穿越 Configuration Change。
4.1 核心原理:Bundle 与 SavedStateHandle
rememberSaveable 的底层依赖于 Android 的 onSaveInstanceState 机制。Compose 通过 SavedStateRegistry 将状态写入 Bundle,当 Activity 重建时,再从 Bundle 中解码并恢复到 MutableState 中。Compose 内置支持直接将 Int、String、Boolean、ArrayList 等基础类型保存到 Bundle 中。
对于复杂对象,必须实现 Parcelable 接口或通过 Saver 手动序列化。下图清晰地展示了 remember 与 rememberSaveable 在配置变更时的行为差异。
4.2 基础实现:直接支持的类型与 Parcelable
对于基本数据类型,使用 rememberSaveable 与 remember 的语法完全一致,Compose 会自动处理 Bundle 的读写。
@Composable
fun CounterScreen() {
// Int 类型的状态会自动保存和恢复
var count by rememberSaveable { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("点击次数: $count")
}
}
对于自定义对象,最直接的方式是让它实现 Parcelable 接口(需要 Kotlin Parcelize 插件支持)。
@Parcelize
data class User(val name: String, val age: Int) : Parcelable
@Composable
fun UserScreen() {
var user by rememberSaveable { mutableStateOf(User("Kotlin", 5)) }
Text("姓名: ${user.name}, 年龄: ${user.age}")
// 屏幕旋转后,数据依然存在
}
4.3 高级技巧:自定义 Saver
当你的数据类无法实现 Parcelable(例如来自第三方库),或者你想用 JSON 等更灵活的方式序列化以减小 Bundle 体积时,就需要自定义 Saver。
Saver 接口定义了两个核心方法:save 用于将对象转换为可存入 Bundle 的类型,restore 用于从 Bundle 中恢复对象。
// 示例:将 User 对象保存为 "name:age" 格式的字符串
val UserSaver = Saver(
save = { user ->
"${user.name}:${user.age}"
},
restore = { savedString ->
val parts = savedString.split(":")
if (parts.size == 2) {
User(parts[0], parts[1].toInt())
} else {
User("Unknown", 0) // 处理数据异常的情况
}
}
)
restore 方法必须做好非空判断和类型转换异常处理,否则当保存的数据结构发生变化时,应用极易发生崩溃。
在 Composable 中应用自定义 Saver
通过 rememberSaveable 的 saver 参数将自定义 Saver 注入即可。
@Composable
fun CustomSaverScreen() {
var user by rememberSaveable(stateSaver = UserSaver) {
mutableStateOf(User("Jetpack", 3))
}
Column {
Text("姓名: ${user.name}")
Text("年龄: ${user.age}")
Button(onClick = { user = user.copy(age = user.age + 1) }) {
Text("增加年龄")
}
}
}
4.4 性能对比与调试技巧
| 特性 | remember | rememberSaveable | rememberSaveable + 自定义 Saver |
|---|---|---|---|
| 配置变更保持 | 否 | 是 | 是 |
| 序列化开销 | 无(内存直接读写) | 自动(Bundle 基础类型) | 由 Saver 实现决定(如 JSON 序列化) |
| 对象类型要求 | 无限制 | 必须为 Bundle 支持的类型或 Parcelable | 无限制(通过 Saver 转换) |
| 适用场景 | 临时 UI 状态(如动画进度) | 必须跨生命周期保持的简单状态 | 复杂对象或无法修改源码的第三方数据类 |
rememberSaveable 是否生效。同时,在 Logcat 中搜索 SavedStateRegistry 可以查看具体的保存与恢复日志。
4.5 常见错误与性能优化
- TransactionTooLargeException: Bundle 传输数据量不能超过 1MB。自定义 Saver 时,尽量避免将大量列表或图片 Base64 数据存入状态中。对于大数据,应改用 Room 等本地持久化方案。
- 状态键冲突: 在同一个 Composable 中多次使用
rememberSaveable时,Compose 会自动生成唯一的键。但如果在一个循环或列表中动态生成 Composable,务必通过key参数显式指定键,否则可能导致状态恢复错乱。 - 避免过度序列化: 如果一个 UI 状态只在当前组合中有效(如滚动位置),但设计上不需要在配置变更后恢复,优先使用
remember以避免不必要的序列化性能开销。
4.6 与 mutableStateOf 和 LaunchedEffect 的协作
rememberSaveable 返回的是一个标准的 MutableState 对象,因此它可以与 LaunchedEffect 等副作用 API 无缝协作。当状态从 Bundle 中恢复后,如果 LaunchedEffect 的键发生变化,它会自动重新执行。
@Composable
fun DataFetchingOnRestore(id: String) {
// 状态会跨越配置变更保持
var data by rememberSaveable { mutableStateOf("") }
// 当 id 改变或进程重建时,重新发起网络请求
LaunchedEffect(id) {
data = fetchDataFromNetwork(id)
}
Text("加载的数据: $data")
}