【问题标题】:Why do I sometimes need key() in lists?为什么有时我需要在列表中使用 key()?
【发布时间】:2021-08-15 16:44:46
【问题描述】:

我有一个带有一些可变状态列表的组件。我将其中的一个项目以及删除该项目的回调传递给另一个组件。

@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 {} 发送唯一密钥。

【问题讨论】:

标签: android android-jetpack-compose android-jetpack-compose-list


【解决方案1】:

Compose 正在尝试通过使用键本地化范围来缓存尽可能多的工作:当它们自上次运行以来没有更改时 - 我们正在使用缓存值,否则我们需要重新计算它。

通过为惰性项设置key,您为内部所有remember 计算定义了一个范围,并且许多系统功能都是使用remember 实现的,因此它发生了很大变化。项目索引是惰性项目中的默认键

因此,在您删除第一个 item 后,第一个惰性项会在与之前相同的上下文中重复使用

现在我们将联系您的myClickable。您将Unit 作为key 传递给pointerInput(它内部也有remember)。通过这样做,您对重构者说:永远不要重新计算这个值,直到上下文发生变化。并且第一个惰性项的上下文没有改变,例如key 仍然是相同的索引,这就是为什么删除了 item 的 lambda 仍然缓存在该函数中

当您指定惰性项 key 等于 item 时,您也在更改所有惰性项的上下文,因此 pointerInput 被重新计算。如果你传递你的item 而不是Unit 你会有同样的效果

所以当你需要使用时你需要使用key,你的计算不会以不好的方式缓存在惰性项之间

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

【讨论】:

  • 谢谢。是的,向pointerEvent() 发送唯一密钥可以解决此问题,而无需在列表中使用key()
【解决方案2】:

Jetpack compose 通过仅重新组合值已更改的 Widget 来优化重新组合。

Modifier.myClickable 的自定义实现中,当项目列表因删除而发生更改时,只有内部的 Text(item, fontSize = 40.sp) 将被重新组合,因为项目已更改并且它是唯一正在读取 item 的项目。外部的 Box() 没有被重组,因此它持有之前的回调。但是当你添加key(item)时,外框也会随着键值的变化而重新组合。因此,它在添加密钥后工作。

那么为什么要使用Modifier.clickable { delete(item) }

我认为 Compose 会跟踪回调 clickable { delete(item) } 中的变化。因此,当回调因项目删除而改变时,它重构了MyComponent,因此正在使用clickable

【讨论】:

  • 感谢您对它如何计算变化的精彩描述。我以前知道这个概念,但你的解释真的帮助我理解它。我不确定为什么 clickable 有效,但正如公认的答案所说,我可以向 pointerInput() 发送一个唯一密钥,并且它可以在外部组件中不使用 key() 的情况下工作。
猜你喜欢
  • 2012-10-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-04-03
  • 2020-02-21
相关资源
最近更新 更多