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 在配置变更时的行为差异。

remember vs rememberSaveable 配置变更对比 1️⃣ 初始重组 (状态 A) 2️⃣ 配置变更 (Activity 重建) 3️⃣ 状态丢失 (回到默认值) 4️⃣ UI 重置/闪烁 1️⃣ 初始重组 (状态 A) 2️⃣ 配置变更 (Activity 重建) 3️⃣ 从 Bundle 恢复状态 4️⃣ UI 无缝恢复至状态 A 结论:使用 rememberSaveable 替代 remember 保持状态持久性

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) // 处理数据异常的情况
        }
    }
)
避坑提示: 自定义 Saver 的 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 性能对比与调试技巧

特性rememberrememberSaveablerememberSaveable + 自定义 Saver
配置变更保持否是是
序列化开销无(内存直接读写)自动(Bundle 基础类型)由 Saver 实现决定(如 JSON 序列化)
对象类型要求无限制必须为 Bundle 支持的类型或 Parcelable无限制(通过 Saver 转换)
适用场景临时 UI 状态(如动画进度)必须跨生命周期保持的简单状态复杂对象或无法修改源码的第三方数据类
调试技巧: 在开发者选项中打开“不保留活动”,点击 Home 键再返回应用,即可模拟进程重建,快速验证 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")
}