深入理解Jetpack Compose key() {} 方法

Jetpack Compose 中 key() 的作用与性能优化解析

在 Jetpack Compose 中,key() 是一个非常重要但容易被忽视的 API。

很多开发者知道:

“列表里最好加 key”

但却不清楚:

  • 它到底解决了什么问题?
  • 不加会发生什么?
  • 它是如何影响 recomposition 的?
  • 它是否真的优化性能?

这篇文章会从 机制 → 问题 → 原理 → 优化效果 → 实战建议 全面讲清楚。


一、Compose 如何判断 UI 的“身份”?

Compose 在重组(Recomposition)时,需要判断:

“这个 Composable 是之前那个吗?”

默认情况下,它使用的是:

调用位置(Call Position)

也就是说:

if (代码位置没变) {
    认为是同一个节点
}

二、问题:动态列表会出错

假设有如下代码:

for (item in list) {
    Text(item.name)
}

如果 list 发生了排序变化:

list = list.sortedBy { it.price }

Compose 会认为:

  • 第 0 个位置还是 Text
  • 第 1 个位置还是 Text

它不知道:

这些 item 已经换人了

这就会导致:

  • remember 状态错位
  • 动画异常
  • 选中状态跑错对象
  • UI 重建异常

三、key() 的核心作用

key(item.id) {
    Text(item.name)
}

加入 key 之后:

Compose 不再通过“位置”判断身份,而是通过:

你提供的 key

对比

无 key 有 key
通过位置判断身份 通过 key 判断身份
列表 reorder 会错位 顺序变化也能正确匹配
remember 可能串数据 状态跟随 item
可能重建 subtree 精准复用节点

四、它解决的核心问题

1️⃣ 避免状态错位

错误示例:

for (stock in stocks) {
    var expanded by remember { mutableStateOf(false) }
}

当 stocks 重排时:

  • expanded 会跟“位置”走
  • A 股票的状态跑到 B 股票上

正确做法:

for (stock in stocks) {
    key(stock.code) {
        var expanded by remember { mutableStateOf(false) }
    }
}

现在:

expanded 会跟 stock.code 走


2️⃣ 减少错误重建

没有 key:

  • Compose 认为每个位置都变了
  • 销毁 + 重建节点

有 key:

  • 只移动节点
  • 不销毁 subtree
  • 状态正确复用

3️⃣ 列表 reorder 性能更稳定

对于:

  • LazyColumn
  • LazyRow
  • HorizontalPager
  • 动态排序列表

key 能避免大量不必要的重建。

⚠️ 注意:

key 并不是“加速 UI”,
它是“避免错误的重建”。


五、和 LazyColumn key 的区别

Lazy API 也有 key:

LazyColumn {
    items(list, key = { it.id }) {
        ...
    }
}

区别:

key() LazyColumn key
普通 Compose 作用域 Lazy 内部 diff 用
手动控制 identity Lazy 自动 diff
影响 remember 影响 item 复用

本质机制相似,但作用域不同。


六、底层原理(进阶理解)

Compose 内部维护一个 SlotTable。

默认:

identity = callPosition

使用 key 后:

identity = callPosition + keyHash

这会影响:

  • remember slot
  • 状态恢复
  • node 复用
  • subtree 移动

七、什么时候必须使用 key()

✅ 动态列表
✅ 列表会 reorder
✅ item 内部有 remember
✅ item 内有动画
✅ Pager 页面切换
✅ 可折叠列表

❌ 静态 UI
❌ 结构固定且顺序不变


八、key() vs remember(key)

很多人会混淆:

key(item.id) { ... }
remember(item.id) { ... }

区别:

API 作用
key() 改变节点身份
remember(key) 控制值是否重算

它们解决的是完全不同的问题。


九、一句话总结

key() 的本质是告诉 Compose:
这个节点是谁,而不是它在第几个位置。

它的价值不是“提升性能”,
而是:

  • 防止状态错位
  • 防止错误重组
  • 保证结构稳定性
  • 减少 subtree 重建

十、结语

如果你在开发:

  • 股票/Trading 列表
  • 可折叠列表
  • 动态排序列表
  • HorizontalPager
  • 带动画的 LazyColumn

那么:

key 不是优化选项,而是结构安全保证。

当你理解 identity 的概念后,你就真正理解了 Compose 的重组机制。