Compose Runtime
Compose 底层 · androidx.compose.runtime
状态与 Snapshot1.13.0-alpha01
mutableStateOf 的 policy、derivedStateOf、snapshotFlow、produceState 与 rememberSaveable 的取舍。
最后更新 2026-08-01
可观察状态
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("$count")
}mutableStateOf 创建 Snapshot 状态。Composable 读取 count 后,后续写入让读取范围失效,在重组阶段重新执行。普通 var 没有通知链——UI 状态必须走可观察状态。
SnapshotMutationPolicy:什么时候算”变了”
val a = mutableStateOf(listOf(1, 2)) // 默认:结构相等
val b = mutableStateOf(listOf(1, 2), structuralEqualityPolicy())
val c = mutableStateOf(obj, referentialEqualityPolicy())
val d = mutableStateOf("", neverEqualPolicy())| Policy | 判定 | 适用 |
|---|---|---|
structuralEqualityPolicy()(默认) | == 相等视为未变 | 数据类、列表(内容相同不触发重组) |
referentialEqualityPolicy() | 只有同一引用才视为未变 | 大对象、懒加载的复杂值 |
neverEqualPolicy() | 每次写入都视为变化 | 每帧都要更新的性能计数 |
关键坑:默认结构相等下,给同一个可变对象 list.add(x) 再赋回去——== 相等 → 不触发重组。要 list + x(新建实例)或改用不可变值。
derivedStateOf:缓存派生结果
val showTopButton by remember {
derivedStateOf { listState.firstVisibleItemIndex > 0 }
}- 计算块里读取的 Snapshot 状态变化时才重算,且结果不变时不通知下游。
- 适合”高频输入 → 低频输出”(滚动位置 → 是否显示按钮)。
- 输入与输出同频时用它是浪费,直接读取即可。
snapshotFlow:把 Snapshot 读变成 Flow
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.distinctUntilChanged()
.collect { index -> analytics.onVisibleIndexChanged(index) }
}snapshotFlow 观察块内读取的状态并发射变化流。用途:
- 接 Flow 操作符(debounce、distinctUntilChanged、map)
- 驱动非 UI 副作用(埋点、日志)
- 与
conflate/collectLatest组合控制节奏
不要用它替代 UI 直接读状态——UI 该用 by remember { mutableStateOf }。
produceState:把一次性异步转成状态
val imageState by produceState<ImageState>(initialValue = Loading, url) {
value = Loading
value = try {
Loaded(repository.loadImage(url))
} catch (e: Exception) {
Error(e)
}
}- 每次
key(url)变化时,块内协程取消重启。 - 适合”从外部数据源加载到本地状态”的封装。
- 复杂场景(多个数据源、需要 ViewModel 级生命周期)应走 ViewModel。
rememberSaveable:跨配置变更/进程恢复
var text by rememberSaveable { mutableStateOf("") } // 自动可存类型
var filter by rememberSaveable(stateSaver = ...) { ... } // 自定义 Saver| 选择 | 生命周期 |
|---|---|
remember | 仅重组存活;配置变更/进程重建丢失 |
rememberSaveable | 配置变更恢复(Bundle);进程重建尽力恢复 |
| ViewModel + StateFlow | 配置变更存活;进程重建由 SavedStateHandle 决定 |
决策:UI 瞬态(滚动位置、输入草稿)用 rememberSaveable;业务数据(列表、登录态)用 ViewModel。不要用 rememberSaveable 存大对象或网络状态。
复制即用
@Composable
fun StatePatterns() {
// 1. 结构相等:内容相同不重组
var text by remember { mutableStateOf("") }
// 2. 派生:滚动决定按钮显隐
val listState = rememberLazyListState()
val showTop by remember {
derivedStateOf { listState.firstVisibleItemIndex > 0 }
}
// 3. 一次性加载转状态
val imageState by produceState<ImageState>(initialValue = ImageState.Loading, url) {
value = ImageState.Loading
value = try {
ImageState.Loaded(loader.load(url))
} catch (e: Exception) {
ImageState.Error(e)
}
}
// 4. 跨配置恢复
var draft by rememberSaveable { mutableStateOf("") }
}常见陷阱
- 把业务状态塞 rememberSaveable:进程重建不保证,且 Bundle 容量有限。
- 每帧创建新对象塞 State:无意义重组。
- derivedStateOf 依赖不该观察的 State:把它当缓存而不当”派生值计算器”,会漏更新。
- 在组合中读多个 State 拼字符串:用 derivedStateOf 减少读取次数。
- 忘 StateFlow 初始值语义:
collectAsStateWithLifecycle首次收集用 initialValue,后续用 Flow 值。
要点
- 默认 policy 是结构相等;可变对象原地修改不会触发重组。
derivedStateOf缓存派生结果,避免下游无意义重组。snapshotFlow把状态读变成可组合的 Flow,用于副作用。produceState封装一次性异步;rememberSaveable只存瞬态。- 业务状态放 ViewModel,UI 瞬态放 rememberSaveable。