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 才可能重组,而非整个调用链。
derivedStateOf 依赖链与重组控制机制 derivedStateOf 依赖链与重组控制机制 搜索关键词 State 数据列表 State derivedStateOf { 过滤 + 排序 } 过滤结果缓存 列表 UI (重组范围) 依赖未变 → 跳过重组 ✓ 重新计算 依赖未变 核心原理:derivedStateOf 自动追踪依赖 State,仅当依赖变化时重新计算并更新缓存;依赖未变时直接返回缓存,下游 Composable 不触发重组。

4.2 实现流程与 API 用法

标准使用步骤

  1. 识别派生来源:确定派生值所依赖的 State 或 MutableState 对象。
  2. 包裹 remember:将 derivedStateOf 放在 remember 中,确保重组时复用同一实例。
  3. 定义计算逻辑:在 lambda 中编写纯计算函数,读取依赖状态并返回派生结果。
  4. 读取 .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 做最后的格式转换或聚合。