第2章 remember 的 key 参数:键变化触发重新计算
Compose 中 remember 的 key 参数是控制缓存生命周期与触发重新计算的“开关”。
只有理解了 key 的比对机制,才能精准地让计算在正确的时机重新执行,同时避免无意义的 UI 重组。
2.1 原理:key 如何驱动重新计算
remember(calculation, key) 内部维护一个键值缓存:
- 首次重组时调用
calculation,将结果与当前key一同存入缓存。 - 后续重组时,用当前 key 与缓存中保存的旧 key 做 equals 比对。
- 若 相等,直接返回缓存值,
calculation不执行; - 若 不相等,重新执行
calculation,更新缓存与 UI。
核心原则:key 的
无 key 参数时,Compose 默认使用
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) }
}
}
💡 性能提示:如果列表很大且频繁增删,建议改用
详见第 4 章
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 参数可以显著减少不必要的计算与重组:
- 最小化 key 粒度:只把真正影响计算结果的变量作为 key,避免无关参数触发重算。
- 优先用稳定类型:
Int、String、data class的equals效率高,避免用大型对象。 - 列表 key 用
toList()快照:不可变列表的equals按结构比对,性能可接受。 - 结合
derivedStateOf:当 key 本身需要派生时,用derivedStateOf生成稳定的派生键。
最佳实践清单:
✅ 每一个
✅ key 类型实现正确的
✅ 用
❌ 避免使用数组、可变集合、Lambda 作为 key
✅ 每一个
remember 都显式写出 key 参数(即使无依赖也写 remember(Unit))✅ key 类型实现正确的
equals + hashCode✅ 用
data class 管理多字段组合键❌ 避免使用数组、可变集合、Lambda 作为 key
2.9 本章小结
- key 是缓存触发的唯一凭据:通过
equals比对决定是否重新执行 calculation。 - 列表与 data class 是推荐的组合键形式:结构相等语义正确、可读性强。
- 避免无意义重组:最小化 key 粒度、使用稳定类型、警惕可变对象和 Lambda 引用。
- 调试时善用日志与 Layout Inspector:快速定位 key 变化引起的额外重组。