第2章  remember 的 key 参数:键变化触发重新计算

Compose 中 remember 的 key 参数是控制缓存生命周期与触发重新计算的“开关”。
只有理解了 key 的比对机制,才能精准地让计算在正确的时机重新执行,同时避免无意义的 UI 重组。

2.1  原理:key 如何驱动重新计算

remember(calculation, key) 内部维护一个键值缓存:

  1. 首次重组时调用 calculation,将结果与当前 key 一同存入缓存。
  2. 后续重组时,用当前 key 与缓存中保存的旧 key 做 equals 比对。
  3. 若 相等,直接返回缓存值,calculation 不执行;
  4. 若 不相等,重新执行 calculation,更新缓存与 UI。
核心原则:key 的 equals 结果是触发重新计算的唯一凭据。
无 key 参数时,Compose 默认使用 Unit 作为固定键,缓存仅在重组期间保持有效。

2.2  键比对与缓存策略

key 类型 比对方式 典型场景 重新计算条件
无 key(默认) Unit.equals 固定计算,重组后不变 不会重新计算
基本类型(Int、String 等) 值相等 计数器、输入值 数值改变
List / 数组 List.equals(结构相等) 列表内容变化时重算 元素或顺序变化
自定义 data class data class.equals(按属性) 多字段组合变化 任意属性变化
Lambda / 对象引用 引用相等(===) 避免意外重算 实例改变时
⚠️ 注意:数组(Array)的 equals 为引用相等,内容变化不会触发重算。
如需按内容触发,应使用 listOf() 或 toList() 转为 List。

2.3  代码示例:基本用法

下面的计数器只在 step 变化时重新计算耗时逻辑:

@Composable
fun CounterWithKey(step: Int) {
    // step 作为 key — 只有 step 改变才重新执行 lambda
    val result = remember(step) {
        println("⚙️ 执行复杂计算 step=$step")
        (1..1_000_000).sumOf { it * step }
    }
    Text("计算结果:$result")
}

2.4  使用列表作为 key

当列表内容变化时需要重新派生数据,将 整个列表 作为 key:

@Composable
fun FilteredList(items: List<String>, query: String) {
    // items 整体作为 key — 列表增删或重排时重新过滤
    val filtered = remember(items, query) {
        println("🔍 重新过滤 query=$query")
        items.filter { it.contains(query, ignoreCase = true) }
    }
    LazyColumn {
        items(filtered) { item -> Text(item) }
    }
}
💡 性能提示:如果列表很大且频繁增删,建议改用 derivedStateOf 加快照订阅,避免整体拷贝。
详见第 4 章 derivedStateOf 与列表派生。

2.5  使用自定义数据类作为 key

当缓存依赖于多个状态字段时,定义一个 data class 作为组合键:

data class SearchParams(
    val query: String,
    val categoryId: Int,
    val sortOrder: SortOrder
)

@Composable
fun SearchResults(params: SearchParams) {
    // data class 自动生成 equals — 任一字段变化都触发重算
    val results = remember(params) {
        println("📦 重新搜索 params=$params")
        repository.search(params.query, params.categoryId, params.sortOrder)
    }
    ResultList(results)
}

使用 data class 避免了手动拼接字符串或维护多个 key 参数的麻烦,代码更安全、可读性更高。

2.6  调试技巧:观察 key 变化

推荐在 remember 的 lambda 内加入日志输出,配合 Layout Inspector 查看重组次数:

@Composable
fun DebugKeyExample(value: Int) {
    val cached = remember(value) {
        println("🔑 key=$value 触发了重新计算")
        heavyComputation(value)
    }
    // 使用 cached ...
}
  • 日志标签:用固定前缀(如 🔑)方便过滤。
  • 重组计数:Android Studio 的 Layout Inspector → Components 树 → Recomposition count。
  • 断点:直接在 remember 的 lambda 内下断点,观察调用栈。

2.7  常见错误与避坑

错误用法 问题说明 正确做法
用可变对象(如 MutableList)做 key equals 不变,内容变化不触发重算 用 toList() 快照或 data class
key 中包含 Lambda 或匿名对象 每次重组生成新实例,引用不同导致频繁重算 用稳定引用或 remember 包裹 Lambda
key 参数过多,用字符串拼接 易出错、可读性差 定义 data class 作为组合键
忘记传 key,依赖外部可变状态 状态变化时缓存不更新,产生逻辑错误 将外部依赖状态作为 key 传入
🚨 关键风险:使用 MutableState 作为 key 时,equals 比对的是 State 对象引用,而非内部值。
正确做法是传入 state.value 或使用 derivedStateOf。

2.8  性能优化:避免无意义重组

合理使用 key 参数可以显著减少不必要的计算与重组:

  1. 最小化 key 粒度:只把真正影响计算结果的变量作为 key,避免无关参数触发重算。
  2. 优先用稳定类型:Int、String、data class 的 equals 效率高,避免用大型对象。
  3. 列表 key 用 toList() 快照:不可变列表的 equals 按结构比对,性能可接受。
  4. 结合 derivedStateOf:当 key 本身需要派生时,用 derivedStateOf 生成稳定的派生键。
最佳实践清单:
✅ 每一个 remember 都显式写出 key 参数(即使无依赖也写 remember(Unit))
✅ key 类型实现正确的 equals + hashCode
✅ 用 data class 管理多字段组合键
❌ 避免使用数组、可变集合、Lambda 作为 key
remember key 触发机制流程图 remember(key) key 值是否变化? 相等(未变) 不相等(变化) 复用缓存值 直接返回缓存 重新执行 calculation 触发 UI 重组 💡 key 的 equals 决定是否重新计算 — 选择稳定、结构相等的类型 避免用可变集合、Lambda、匿名对象作为 key,防止意外重算或缓存失效

2.9  本章小结

  • key 是缓存触发的唯一凭据:通过 equals 比对决定是否重新执行 calculation。
  • 列表与 data class 是推荐的组合键形式:结构相等语义正确、可读性强。
  • 避免无意义重组:最小化 key 粒度、使用稳定类型、警惕可变对象和 Lambda 引用。
  • 调试时善用日志与 Layout Inspector:快速定位 key 变化引起的额外重组。