您可能会遇到常见的 Compose 问题。这些错误可能会给您的代码 看起来运行良好,但可能会影响界面性能。关注最佳商家 在 Compose 上优化应用的最佳实践。
使用 remember
尽可能减少开销高昂的计算
可组合函数可能会非常频繁地运行,与每个帧的运行频率一样高 动画效果因此,您应当在可组合函数的主体部分中尽可能减少计算。
使用 存储计算结果
remember
。这样,计算只会运行一次,
结果。
例如,下面的代码显示了经过排序的名称列表,但 进行大量排序:
@Composable fun ContactList( contacts: List<Contact>, comparator: Comparator<Contact>, modifier: Modifier = Modifier ) { LazyColumn(modifier) { // DON’T DO THIS items(contacts.sortedWith(comparator)) { contact -> // ... } } }
每次重组 ContactsList
时,都会对整个联系人列表全部排序
即使该列表没有变化如果用户滚动列表
每当出现新行时,可组合函数都会重组。
如需解决此问题,请在 LazyColumn
外部对列表进行排序,并使用 remember
存储已排序列表:
@Composable fun ContactList( contacts: List<Contact>, comparator: Comparator<Contact>, modifier: Modifier = Modifier ) { val sortedContacts = remember(contacts, comparator) { contacts.sortedWith(comparator) } LazyColumn(modifier) { items(sortedContacts) { // ... } } }
现在,列表只会在 ContactList
首次组合时执行一次排序。如果联系人或比较器发生变化,则系统会重新生成经过排序的列表。否则,可组合函数会继续使用缓存中的已排序列表。
使用延迟布局键
延迟布局可高效地重复使用项,只需重新生成或重组项 。不过,您可以帮助优化延迟布局 重组。
假设某项用户操作会导致项在列表中移动。例如: 假设您显示了按修改时间排序的 上面显示了最近修改过的备注
@Composable fun NotesList(notes: List<Note>) { LazyColumn { items( items = notes ) { note -> NoteRow(note) } } }
不过,此代码存在问题。假设底部的备注发生了变化。 它现在是最近修改过的备注,因此会移到列表顶部,而其他备注都会向下移动一个位置。
如果没有您的帮助,Compose 不会意识到未更改的项 moved。相反,Compose 会认为旧的“第 2 项”已被删除,且 为第 3 项、第 4 项以及最下面的一项创建了新的订单。其结果是 Compose 会重组列表中的每一项,即使其中只有一项重组 实际上都发生了变化
此处的解决方案是提供项键。为 每一项都可让 Compose 避免不必要的重组。在本例中,Compose 可以确定现在位于位置 3 的物品与以前位于位置 2 的物品相同。 由于该项的数据没有任何变化,因此 Compose 不必 重组。
@Composable fun NotesList(notes: List<Note>) { LazyColumn { items( items = notes, key = { note -> // Return a stable, unique key for the note note.id } ) { note -> NoteRow(note) } } }
使用 derivedStateOf
限制重组
在组合中使用状态的一个风险是,如果状态发生变化, 界面的重组次数可能会超出您的实际需求。例如: 假设您要显示一个可滚动列表。您可以检查列表的状态, 哪个项是列表中的第一个可见项:
val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } val showButton = listState.firstVisibleItemIndex > 0 AnimatedVisibility(visible = showButton) { ScrollToTopButton() }
问题在于,当用户滚动列表时,listState
会随着用户拖动手指而不断变化。这意味着该列表会不断重组。不过,您不需要频繁重组代码,只需
无需重组,直至新项显示在底部为止。因此,这将完成大量的额外计算,从而导致界面性能较差。
解决方案是使用派生状态。借助派生状态,您可以告知 Compose 哪些状态更改实际上应触发重组。在此示例中 指明您在第一个可见项发生更改时您关心。如果那 状态值发生变化时,界面需要重组,但如果用户尚未重组 使新项显示在顶部,则无需重组。
val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } val showButton by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } } AnimatedVisibility(visible = showButton) { ScrollToTopButton() }
尽可能延迟读取
发现性能问题后,延后读取状态会有所帮助。延后读取状态可以确保 Compose 在重组时重新运行尽可能少的代码。例如,如果界面的状态在可组合项树中向上提升,而您在可组合子项中读取状态,则可以将状态封装在 lambda 函数中。这种方式可以确保仅在实际需要时才会执行读取操作。有关参考,请参阅 Jetsnack 中的 示例应用。Jetsnack 实现了类似于工具栏的折叠效果 显示详细信息屏幕要了解这种方法起作用的原因,请参阅这篇博文 Jetpack Compose:调试重组。
为了实现这种效果,Title
可组合项需要滚动偏移
以便使用 Modifier
自行偏移。这是
进行优化之前的 Jetsnack 代码:
@Composable fun SnackDetail() { // ... Box(Modifier.fillMaxSize()) { // Recomposition Scope Start val scroll = rememberScrollState(0) // ... Title(snack, scroll.value) // ... } // Recomposition Scope End } @Composable private fun Title(snack: Snack, scroll: Int) { // ... val offset = with(LocalDensity.current) { scroll.toDp() } Column( modifier = Modifier .offset(y = offset) ) { // ... } }
当滚动状态发生变化时,Compose 会使最近的父项失效
重组作用域。在本例中,最接近的范围是 SnackDetail
可组合项。请注意,Box
是内联函数,因此不是重组
范围。因此 Compose 会重组 SnackDetail
以及其中的所有可组合项
SnackDetail
。如果您将代码更改为仅读取
使用它,那么您可以减少需要重组的元素数量。
@Composable fun SnackDetail() { // ... Box(Modifier.fillMaxSize()) { // Recomposition Scope Start val scroll = rememberScrollState(0) // ... Title(snack) { scroll.value } // ... } // Recomposition Scope End } @Composable private fun Title(snack: Snack, scrollProvider: () -> Int) { // ... val offset = with(LocalDensity.current) { scrollProvider().toDp() } Column( modifier = Modifier .offset(y = offset) ) { // ... } }
滚动参数现在是一个 lambda。这意味着 Title
仍然可以引用提升的状态,但该值仅在 Title
内部读取,这也是实际需要的。因此,当滚动值发生更改时,最近的重组范围现在是 Title
可组合项 - Compose 不再需要重组整个 Box
。
这是一项非常重大的改进,但是您还可以做得更好!如果您触发重组只是为了重新布局或重新绘制可组合项,那么您肯定会充满了疑惑。在本例中,您只是更改了 Title
可组合项的偏移量,而此操作可以在布局阶段完成。
@Composable private fun Title(snack: Snack, scrollProvider: () -> Int) { // ... Column( modifier = Modifier .offset { IntOffset(x = 0, y = scrollProvider()) } ) { // ... } }
以前,该代码使用 Modifier.offset(x: Dp, y: Dp)
,它接受
以参数形式指定偏移量通过切换到 lambda 版本的修饰符,
您可以确保该函数在布局阶段读取滚动状态。因此,当滚动状态发生变化时,Compose 可以完全跳过组合阶段,而直接进入布局阶段。当您将频繁更改的状态变量传递到修饰符中时,应当尽可能使用其 lambda 版本。
下面给出了此方法的另一个示例。此代码尚未优化:
// Here, assume animateColorBetween() is a function that swaps between // two colors val color by animateColorBetween(Color.Cyan, Color.Magenta) Box( Modifier .fillMaxSize() .background(color) )
在此代码中,Box 的背景颜色会在两种颜色之间快速切换。因此,其状态也会非常频繁地变化。随后,可组合项会在后台修饰符中读取此状态。因此,该 Box 在每一帧上都需要重组,因为其颜色在每一帧中都会发生变化。
如需改进这一点,请使用基于 lambda 的修饰符,在本例中为 drawBehind
。
这将仅在绘制阶段读取颜色状态。因此
Compose 可以完全跳过组合和布局阶段 -
Compose 会直接进入绘制阶段。
val color by animateColorBetween(Color.Cyan, Color.Magenta) Box( Modifier .fillMaxSize() .drawBehind { drawRect(color) } )
避免向后写入
Compose 有一项核心假设,即您永远不会向已被读取的状态写入数据。此操作被称为向后写入,它可能会导致无限次地在每一帧上进行重组。
以下可组合项展示了此类错误的示例。
@Composable fun BadComposable() { var count by remember { mutableStateOf(0) } // Causes recomposition on click Button(onClick = { count++ }, Modifier.wrapContentSize()) { Text("Recompose") } Text("$count") count++ // Backwards write, writing to state after it has been read</b> }
此代码会在可组合项的 前一行。如果运行此代码,您会看到 按钮,这会导致重组,则计数器会快速增加, 无限循环,因为 Compose 重组此可组合项,看到状态读取 已过期,所以我们安排了另一次重组。
您完全可以避免向后写入数据,只需避免在组合中写入状态即可。请尽可能在响应事件时写入状态,并采用 lambda 的形式,如上文中的 onClick
示例所示。
其他资源
为您推荐
- 注意:当 JavaScript 处于关闭状态时,系统会显示链接文字
- 状态和 Jetpack Compose
- 图形修饰符
- Compose 编程思想