为什么我有时需要在列表中使用 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 你的计算不会以糟糕的方式缓存在惰性项目之间

documentation

中查看有关惰性列键的更多信息