State Hoisting避免重组合实战 - 第4节
4.1 原理:derivedStateOf 的缓存与惰性计算机制
derivedStateOf 是 Jetpack Compose 中用于从现有状态派生新状态的纯计算 API。它通过自动追踪依赖的 State<T> 对象,在读取时惰性计算派生值,并将结果缓存。当依赖状态发生变化时,Compose 的快照系统会使缓存失效,下一次读取时重新计算;若依赖未变,则直接返回缓存值,从而避免下游 Composable 因无关状态变化而触发重组。
核心优势:derivedStateOf 将“计算”与“重组”解耦——只有真正读取派生结果的 Composable 才会被纳入重组范围,且仅当派生值实际变化时才触发重组,而非每次依赖状态变化都重组。
缓存与惰性计算
- 惰性计算:derivedStateOf 的 lambda 只在第一次读取
.value时执行,之后依赖未变则直接返回缓存。 - 自动失效:Compose 的
Snapshot系统记录 lambda 中读取的所有State对象,当任意依赖被修改时标记派生结果为“脏”,下次读取时重新计算。 - 重组范围缩窄:使用 derivedStateOf 后,只有读取派生结果的 Composable 才可能重组,而非整个调用链。
4.2 实现流程与 API 用法
标准使用步骤
- 识别派生来源:确定派生值所依赖的
State或MutableState对象。 - 包裹 remember:将
derivedStateOf放在remember中,确保重组时复用同一实例。 - 定义计算逻辑:在 lambda 中编写纯计算函数,读取依赖状态并返回派生结果。
- 读取 .value:在 Composable 中通过
.value读取缓存结果。
@Composable
fun SearchResultList(allItems: List<String>, query: String) {
// 步骤 2 + 3:使用 remember + derivedStateOf 定义派生状态
val filteredItems = remember {
derivedStateOf {
if (query.isBlank()) {
allItems
} else {
allItems.filter { it.contains(query, ignoreCase = true) }
}
}
}
// 步骤 4:读取 .value,Composable 仅在此处依赖派生结果
LazyColumn {
items(filteredItems.value) { item ->
Text(text = item, modifier = Modifier.padding(8.dp))
}
}
}
API 签名:
fun <T> derivedStateOf(calculation: () -> T): State<T>。返回的 State<T> 是只读的,不可直接修改;计算逻辑必须是无副作用的纯函数。
4.3 依赖链优化策略
当派生逻辑需要多层变换时,拆分依赖链 可以显著缩小重组范围。每层 derivedStateOf 只追踪其直接依赖,下游仅在上游派生值变化时重组,而非原始状态变化时重组。
@Composable
fun Dashboard(stats: Stats) {
// 第一层:从原始 State 派生基础指标
val activeCount = remember {
derivedStateOf { stats.items.count { it.active } }
}
// 第二层:基于第一层派生汇总文本
val summary = remember {
derivedStateOf {
"活跃:${activeCount.value} / 总计:${stats.items.size}"
}
}
// 第三层:仅在 summary 变化时重组
Text(text = summary.value, style = MaterialTheme.typography.h6)
}
| 优化方式 | 重组范围 | 计算次数 | 适用场景 |
|---|---|---|---|
| 单层 derivedStateOf | 仅读取处重组 | 依依赖变化 | 简单过滤、映射 |
| 多层依赖链拆分 | 每层独立追踪 | 按需逐层计算 | 复杂指标、级联派生 |
| 不使用 derivedStateOf | 整个 Composable | 每次重组都计算 | 不推荐 |
4.4 调试与重组观察
使用 Layout Inspector 观察重组
在 Android Studio 中运行应用后,通过 Tools → Layout Inspector → Show Recomposition Counts 可以直观看到每个 Composable 的重组次数。使用 derivedStateOf 后,下游节点的重组次数应明显下降。
日志插桩验证
@Composable
fun DebugDerivedState(counter: State<Int>) {
val isEven = remember {
derivedStateOf {
println("♻️ derivedStateOf 重新计算: ${counter.value}")
counter.value % 2 == 0
}
}
println("📱 Composable 重组, isEven=${isEven.value}")
Text(text = if (isEven.value) "偶数" else "奇数")
}
注意:生产环境中请移除调试日志。derivedStateOf 的 lambda 可能在后台线程执行,避免在其中写入日志影响性能分析。
4.5 常见错误与避坑指南
错误 1:在 derivedStateOf 中引入副作用
// ❌ 错误:derivedStateOf 不应包含副作用
val bad = remember {
derivedStateOf {
Log.d("TAG", "计算了") // 副作用!
analytics.track("派生") // 副作用!
items.filter { it.active }
}
}
// ✅ 正确:保持纯函数,副作用移出
val good = remember {
derivedStateOf {
items.filter { it.active }
}
}
错误 2:忘记包裹 remember
// ❌ 错误:每次重组都创建新的 derivedStateOf 实例
val result = derivedStateOf { a.value + b.value }
// ✅ 正确:使用 remember 保持实例稳定
val result = remember { derivedStateOf { a.value + b.value } }
错误 3:依赖链中使用可变对象
// ❌ 错误:依赖 MutableList 的引用而非内容
val list = remember { mutableStateListOf<String>() }
val size = remember { derivedStateOf { list.size } } // 仅当 list 引用变化时触发
// ✅ 正确:将不可变数据作为依赖
val items by remember { mutableStateOf(listOf<String>()) }
val size = remember { derivedStateOf { items.size } }
4.6 性能优化最佳实践
- 浅依赖优先:让 derivedStateOf 的 lambda 只读取必要的 State 字段,减少不必要的失效。
- 避免昂贵计算:如果派生计算耗时较长(如排序、正则匹配),考虑使用
LaunchedEffect+snapshotFlow在后台协程计算,再将结果写入 State。 - 合理拆分:当多个 Composable 需要同一派生值时,将 derivedStateOf 提升到公共父级,避免重复计算。
- 警惕隐式依赖:lambda 中访问的局部变量若并非 State 对象,不会被快照追踪,可能导致缓存不一致。
// 优化示例:将耗时计算移至协程,derivedStateOf 只做轻量映射
@Composable
fun HeavyDerivedExample(rawData: State<List<Data>>) {
var processed by remember { mutableStateOf<List<Result>>(emptyList()) }
LaunchedEffect(rawData.value) {
withContext(Dispatchers.Default) {
processed = rawData.value.map { heavyTransform(it) }
}
}
val summary = remember {
derivedStateOf { "已完成 ${processed.size} 项处理" }
}
Text(summary.value)
}
优化总结:derivedStateOf 的最佳使用场景是轻量、频繁读取的派生状态。对于计算开销大的逻辑,应配合
LaunchedEffect 或 snapshotFlow 做异步计算,将最终结果存入普通 State,再用 derivedStateOf 做最后的格式转换或聚合。