为什么我有时需要在列表中使用 key()?
Why do I sometimes need key() in lists?
我有一个带有一些可变状态列表的组件。我将其中的一个项目和一个删除该项目的回调传递给另一个组件。
@Composable
fun MyApp() {
val myItems = mutableStateListOf("1", "2", "3")
LazyColumn {
items(myItems) { item ->
MyComponent(item) { toDel -> myItems.remove(toDel) }
}
}
}
组件在 clickable
修饰符中调用 delete
回调。
@Composable
fun MyComponent(item: String, delete: (String) -> Unit = {}) {
Column {
Box(
Modifier
.size(200.dp)
.background(MaterialTheme.colors.primary)
.clickable { delete(item) }
) {
Text(item, fontSize = 40.sp)
}
}
}
这很好用。但是当我用 pointerInput()
为我自己的修饰符更改 clickable
时,就会出现问题。
fun Modifier.myClickable(delete: () -> Unit) =
pointerInput(Unit) {
awaitPointerEventScope { awaitFirstDown() }
delete()
}
@Composable
fun MyComponent(item: String, delete: (String) -> Unit = {}) {
Column {
Box(
Modifier
.size(200.dp)
.background(MaterialTheme.colors.primary)
.myClickable { delete(item) } // NEW
) {
Text(item, fontSize = 40.sp)
}
}
}
如果我单击第一个项目,它将被删除。接下来,如果我单击最新的顶部项目,则会调用现在已删除的第一个项目的旧回调,尽管旧组件已被删除。
我不知道为什么会这样。但我可以修复它。我使用 key()
:
@Composable
fun MyApp() {
val myItems = mutableStateListOf("1", "2", "3")
LazyColumn {
items(myItems) { item ->
key(item) { // NEW
MyComponent(item) { toDel -> myItems.remove(toDel) }
}
}
}
}
那么为什么我在使用自己的修饰符时需要 key()
? jetpack的这段代码也是如此,我也不知道为什么。
正如接受的答案所说,Compose 不会重新计算我的自定义修饰符,因为 pointerEvent()
没有唯一键。
fun Modifier.myClickable(key:Any? = null, delete: () -> Unit) =
pointerInput(key) {
awaitPointerEventScope { awaitFirstDown() }
delete()
}
和
Box(
Modifier
.size(200.dp)
.background(MaterialTheme.colors.primary)
.myClickable(key = item) { delete(item) } // NEW
) {
Text(item, fontSize = 40.sp)
}
修复它,我不需要在外部组件中使用 key()
。但是,我仍然不确定为什么不需要向 clickable {}
发送唯一密钥。
Jetpack compose 通过仅重构值已更改的 Widget 来优化重构。
在 Modifier.myClickable
的自定义实现中,当项目列表因删除而更改时,只有内部 Text(item, fontSize = 40.sp)
会被重新组合,因为项目已更改并且它是唯一正在读取 item
。外部 Box()
没有重组,因此它保留了之前的回调。但是当你加上key(item)
的时候,外框也会随着键值的改变而重新组合。因此它在添加密钥后工作。
那么为什么要与 Modifier.clickable { delete(item) }
合作?
我认为 Compose 会跟踪回调中的变化 clickable { delete(item) }
。因此,当回调因项目删除而更改时,它会重组 MyComponent
,因此正在使用 clickable
Compose 正在尝试通过使用键本地化范围来缓存尽可能多的工作:当它们自上次以来没有变化时 运行 - 我们正在使用缓存值,否则我们需要重新计算它。
通过为惰性项设置key
,您为内部的所有remember
计算定义了一个范围,并且许多系统功能是使用remember
实现的,因此它的变化很大。项目索引是惰性项目中的默认键
因此,在您删除第一个 item
之后,第一个惰性项将在与之前相同的上下文中重复使用
现在我们来到您的myClickable
。您将 Unit
作为 key
传递给 pointerInput
(它内部也有一个 remember
)。通过这样做,您是在对重组者说:在上下文发生变化之前,永远不要重新计算该值。并且第一个懒惰项目的上下文没有改变,例如key
仍然是相同的索引,这就是为什么删除了 item
的 lambda 仍然缓存在该函数中的原因
当您指定惰性项 key
等于 item
时,您也在更改所有惰性项的上下文,因此 pointerInput
得到重新计算。如果您传递 item
而不是 Unit
,您将获得相同的效果
所以当你需要使用你的计算时你需要使用 key
你的计算不会以糟糕的方式缓存在惰性项目之间
中查看有关惰性列键的更多信息
我有一个带有一些可变状态列表的组件。我将其中的一个项目和一个删除该项目的回调传递给另一个组件。
@Composable
fun MyApp() {
val myItems = mutableStateListOf("1", "2", "3")
LazyColumn {
items(myItems) { item ->
MyComponent(item) { toDel -> myItems.remove(toDel) }
}
}
}
组件在 clickable
修饰符中调用 delete
回调。
@Composable
fun MyComponent(item: String, delete: (String) -> Unit = {}) {
Column {
Box(
Modifier
.size(200.dp)
.background(MaterialTheme.colors.primary)
.clickable { delete(item) }
) {
Text(item, fontSize = 40.sp)
}
}
}
这很好用。但是当我用 pointerInput()
为我自己的修饰符更改 clickable
时,就会出现问题。
fun Modifier.myClickable(delete: () -> Unit) =
pointerInput(Unit) {
awaitPointerEventScope { awaitFirstDown() }
delete()
}
@Composable
fun MyComponent(item: String, delete: (String) -> Unit = {}) {
Column {
Box(
Modifier
.size(200.dp)
.background(MaterialTheme.colors.primary)
.myClickable { delete(item) } // NEW
) {
Text(item, fontSize = 40.sp)
}
}
}
如果我单击第一个项目,它将被删除。接下来,如果我单击最新的顶部项目,则会调用现在已删除的第一个项目的旧回调,尽管旧组件已被删除。
我不知道为什么会这样。但我可以修复它。我使用 key()
:
@Composable
fun MyApp() {
val myItems = mutableStateListOf("1", "2", "3")
LazyColumn {
items(myItems) { item ->
key(item) { // NEW
MyComponent(item) { toDel -> myItems.remove(toDel) }
}
}
}
}
那么为什么我在使用自己的修饰符时需要 key()
? jetpack的这段代码也是如此,我也不知道为什么。
正如接受的答案所说,Compose 不会重新计算我的自定义修饰符,因为 pointerEvent()
没有唯一键。
fun Modifier.myClickable(key:Any? = null, delete: () -> Unit) =
pointerInput(key) {
awaitPointerEventScope { awaitFirstDown() }
delete()
}
和
Box(
Modifier
.size(200.dp)
.background(MaterialTheme.colors.primary)
.myClickable(key = item) { delete(item) } // NEW
) {
Text(item, fontSize = 40.sp)
}
修复它,我不需要在外部组件中使用 key()
。但是,我仍然不确定为什么不需要向 clickable {}
发送唯一密钥。
Jetpack compose 通过仅重构值已更改的 Widget 来优化重构。
在 Modifier.myClickable
的自定义实现中,当项目列表因删除而更改时,只有内部 Text(item, fontSize = 40.sp)
会被重新组合,因为项目已更改并且它是唯一正在读取 item
。外部 Box()
没有重组,因此它保留了之前的回调。但是当你加上key(item)
的时候,外框也会随着键值的改变而重新组合。因此它在添加密钥后工作。
那么为什么要与 Modifier.clickable { delete(item) }
合作?
我认为 Compose 会跟踪回调中的变化 clickable { delete(item) }
。因此,当回调因项目删除而更改时,它会重组 MyComponent
,因此正在使用 clickable
Compose 正在尝试通过使用键本地化范围来缓存尽可能多的工作:当它们自上次以来没有变化时 运行 - 我们正在使用缓存值,否则我们需要重新计算它。
通过为惰性项设置key
,您为内部的所有remember
计算定义了一个范围,并且许多系统功能是使用remember
实现的,因此它的变化很大。项目索引是惰性项目中的默认键
因此,在您删除第一个 item
之后,第一个惰性项将在与之前相同的上下文中重复使用
现在我们来到您的myClickable
。您将 Unit
作为 key
传递给 pointerInput
(它内部也有一个 remember
)。通过这样做,您是在对重组者说:在上下文发生变化之前,永远不要重新计算该值。并且第一个懒惰项目的上下文没有改变,例如key
仍然是相同的索引,这就是为什么删除了 item
的 lambda 仍然缓存在该函数中的原因
当您指定惰性项 key
等于 item
时,您也在更改所有惰性项的上下文,因此 pointerInput
得到重新计算。如果您传递 item
而不是 Unit
,您将获得相同的效果
所以当你需要使用你的计算时你需要使用 key
你的计算不会以糟糕的方式缓存在惰性项目之间