Jetpack Compose 状态管理
Jetpack Compose 正在改变我们构建 Android UI 的方式。其声明式的特性让我们能更直观地描述界面,但同时也带来了一个核心问题:如何管理和持久化UI状态?
Part 1: remember - Compose 的"短期记忆"
在 Compose 中,当一个状态改变时,相关的 Composable 函数会重组(Recomposition),也就是重新执行。这意味着,任何在函数体内的普通变量都会在每次重组时被重置。
为了解决这个问题,Compose 提供了 remember。
remember 的核心作用是在多次重组之间"记住"一个值。它会将计算结果存储在组合树中,下次重组时直接返回缓存的值,而不是重新计算。
用例 1:记住 UI 状态
这是最常见的用法。remember 与 MutableState 结合,让状态在重组后得以保留。
kotlin
// 如果没有 remember,每次重组 name 都会被重置为空字符串
var name by remember { mutableStateOf("") }```
#### 用例 2:缓存高成本计算
`remember` 不仅能记住宿主,还能记住任何计算结果,这对于避免重复执行昂贵操作至关重要。
```kotlin
// 创建 ShaderBrush 的操作(特别是加载位图)是昂贵的
// 使用 remember 可以确保它只在首次组合时被创建一次
val brush = remember {
ShaderBrush(
BitmapShader(
ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(),
Shader.TileMode.REPEAT,
Shader.TileMode.REPEAT
)
)
}
remember的局限 :它的记忆是"短期"的。当发生配置变更(如屏幕旋转)或系统因内存不足终止进程 时,remember记住的所有内容都会丢失。
Part 2: rememberSaveable - 跨越配置变更的"长期记忆"
为了解决 remember 的局限性,Compose 提供了 rememberSaveable。
顾名思义,rememberSaveable 不仅能在重组中记住状态,还能自动将数据保存到 Android 的 Bundle 中。这意味着即使用户旋转了屏幕,甚至应用进程被系统回收后,状态也能够被完美恢复。
kotlin
// 即使用户旋转屏幕,输入框中的文字也不会丢失
var name by rememberSaveable { mutableStateOf("") }
Part 3: 让自定义对象变得"可保存"
rememberSaveable 对基本数据类型(Int, String, Boolean...)支持得很好,但如果我们想保存一个自定义的 City 对象呢?默认情况下,rememberSaveable 是做不到的,因为它不知道如何将一个 City 对象存入 Bundle。
我们有以下三种主流方案来解决这个问题。
方案一:@Parcelize - 自动化实现 (最推荐)
如果我们可以修改这个类的源码,这是最简单、最优雅的方式。通过添加 kotlin-parcelize 插件并为数据类添加 @Parcelize 注解,编译器会自动为我们实现 Parcelable 接口。
kotlin
// 1. 在 build.gradle 中启用插件
plugins {
id 'kotlin-parcelize'
}
// 2. 为数据类添加注解并实现 Parcelable 接口
@Parcelize
data class City(val name: String, val country: String) : Parcelable
// 3. 直接在 rememberSaveable 中使用
@Composable
fun CityScreen() {
var selectedCity = rememberSaveable {
mutableStateOf(City("Madrid", "Spain"))
}
}
rememberSaveable 会自动检测到 City 是 Parcelable 的,并知道如何处理它。
方案二:自定义 Saver - 当你无法修改源码时
当我们无法修改类的定义时(例如,它来自第三方库),我们可以提供一个自定义的 Saver 对象,明确告诉 rememberSaveable 如何"保存"和"恢复"这个对象。
MapSaver - 健壮且可读的选择
MapSaver 将对象转换为一个 Map<String, Any>。因为值是通过键来存取的,所以它不依赖于顺序,非常健壮。
kotlin
val CitySaver = mapSaver(
save = { city ->
// 保存:将 City 对象拆解成一个 Map
mapOf("Name" to city.name, "Country" to city.country)
},
restore = { map ->
// 恢复:从 Map 中读取值,重新构建 City 对象
City(map["Name"] as String, map["Country"] as String)
}
)
// 使用时,将自定义的 Saver 传递给 stateSaver 参数
var selectedCity = rememberSaveable(stateSaver = CitySaver) {
mutableStateOf(City("Madrid", "Spain"))
}
ListSaver - 简洁但需谨慎的选择
ListSaver 将对象转换为一个 List<Any>。它通过索引来存取,代码更简洁,但也更"脆弱"。
kotlin
val CitySaver = listSaver<City, Any>(
save = {
// 保存:将属性按固定顺序放入列表
listOf(it.name, it.country)
},
restore = {
// 恢复:按相同顺序从列表中通过索引取值
City(it[0] as String, it[1] as String)
}
)
// 使用方法与 MapSaver 相同
var selectedCity = rememberSaveable(stateSaver = CitySaver) {
mutableStateOf(City("Madrid", "Spain"))
}
| 特性 | MapSaver | ListSaver |
|---|---|---|
| 工作方式 | 键值对 (Key-Value) | 有序列表 (Index-based) |
| 健壮性 | 高,不依赖属性顺序 | 低,顺序改变会导致错误 |
| 代码量 | 略多 | 非常简洁 |
| 推荐场景 | 大多数自定义对象 | 属性极少且顺序非常稳定的对象 |
Part 4: 精准控制:当状态需要"重置"时
remember 和 rememberSaveable 都会一直持有缓存的值,直到它们离开组合树。但有时我们希望在输入参数变化时,主动重置或重新计算这个状态 。这就是 key 和 inputs 参数的用武之地。
remember 与 key
当 remember 的 key 发生变化时,它会废弃旧的缓存,并重新执行其 lambda 计算块。
kotlin
// avatarRes 是一个外部传入的图片资源 ID
val brush = remember(key1 = avatarRes) {
// 当 avatarRes 改变时,这段代码会重新执行,
// 创建一个新的、包含新头像的 ShaderBrush
ShaderBrush(...)
}
rememberSaveable 与 inputs
rememberSaveable 有着同样的能力,参数名为 inputs。当 inputs 变化时,rememberSaveable 会丢弃已保存的状态,并重新执行初始化。
这在"基于外部数据初始化,但允许用户本地修改"的场景下非常有用。
kotlin
// typedQuery 是外部传入的搜索词
var userTypedQuery by rememberSaveable(inputs = typedQuery, stateSaver = TextFieldValue.Saver) {
// 当外部的 typedQuery 改变时,文本框的内容会被重置为这个新值
mutableStateOf(
TextFieldValue(text = typedQuery, selection = TextRange(typedQuery.length))
)
}
Part 5: 架构与最佳实践 - 状态应该放在哪里?
我们已经掌握了管理状态的工具,但更重要的问题是:状态应该放在哪里?
状态提升 (State Hoisting)
这是一个核心原则:将状态从子 Composable 中"提升"到它们的共同父 Composable 中。这样做的好处是:
- 单一数据源:状态由一个地方统一管理。
- 可复用性 :子 Composable 变为无状态 (Stateless) 的,它们只负责接收数据并显示,不关心数据从何而来,因此可以在任何地方复用。
- 可测试性:无状态的 Composable 更容易进行单元测试。
ViewModel - 屏幕级别状态的最终归宿
对于屏幕级别的状态(那些与业务逻辑紧密相关的状态),最佳实践是将其放在 ViewModel 中。
ViewModel天然具备生命周期感知能力,它能在配置变更后存活,完美解决了状态持久化的问题。- 它将业务逻辑与 UI 逻辑分离,使代码结构更清晰。
- UI (Composable) 通过
StateFlow或其他可观察类型来订阅ViewModel中的状态,并对其做出反应。
结论
掌握状态管理是精通 Jetpack Compose 的关键。让我们快速回顾一下核心工具和使用场景:
remember: 用于处理仅与 UI 相关 、临时性 的状态,以及缓存昂贵的计算。rememberSaveable: 用于需要跨越配置变更和进程终止来持久化的状态,如用户输入。@Parcelize,MapSaver,ListSaver: 用于让rememberSaveable支持你的自定义数据类型。key/inputs: 用于在外部依赖变化时,精准地重置或重新计算被记住的状态。- 状态提升 &
ViewModel: 遵循这些架构原则,构建可维护、可测试、结构清晰的 Compose 应用。