起初,屏幕看起来很正常。没有崩溃。没有 ANR(应用程序无响应)。也没有明显的帧丢失。但是,每次我滚动、输入或更改滤镜时,我总感觉有些不对劲------就像 UI 在做更多的工作。我打开 Compose 布局检查器,期望找到一个坏的 Composable 组件。但实际上,我发现整个屏幕一直在悄悄地重新组合,这都是因为三个微小的错误,在代码审查中看起来完全无害。
核心要点
本文介绍了在 Jetpack Compose 中不必要的重组现象,以及如何通过使用稳定模型、 remember 和 derivedStateOf 来减少重组。
读者将学到什么
- 为什么重组本身不是坏事
- 不稳定的参数如何导致不必要的重组
- 为什么在可组合项中创建对象可能是昂贵的
- 何时使用
remember - 何时使用
derivedStateOf - 如何思考在可组合项中传递 lambda
第一部分:问题
在 Compose 中,UI 是状态的一个函数。当状态发生变化时,Compose 会重新运行受影响的 composable 函数,这是正常的。问题出现在当不相关的 UI 也重新组合时,因为 Compose 无法证明你的输入是稳定或未改变的。
示例情况:
- 一个产品列表屏幕有一个搜索框
- 输入一个字符更新查询
- 即使项目数据没有变化,完整的项目列表 UI 也会重新组合
- 排序/筛选会反复创建新的列表对象
- Lambda 函数在每次重新组合时都会被重新创建
第二节:错误 1------传递不稳定的模型
优化之前
c
data class ProductUiModel(
val id: String,
val title: String,
val price: Double,
val tags: List<String>
)
@Composable
fun ProductCard(product: ProductUiModel) {
Column {
Text(product.title)
Text("$${product.price}")
}
}
这样看起来没问题,但 List可能会被 Compose 视为不稳定,因为它无法始终保证列表不会发生变更。
优化之后
c
import androidx.compose.runtime.Immutable
@Immutable
data class ProductUiModel(
val id: String,
val title: String,
val price: Double,
val tags: ImmutableList<String>
)
尽可能使用不可变 UI 模型。当 Compose 能够推理稳定性时,它可以跳过更多工作。避免将可变集合传递给深层嵌套的可组合项。
第 3 节:错误 2------在重组期间创建昂贵的对象
优化之前
c
@Composable
fun ProductList(products: List<ProductUiModel>, query: String) {
val filteredProducts = products.filter {
it.title.contains(query, ignoreCase = true)
}
LazyColumn {
items(filteredProducts) { product ->
ProductCard(product)
}
}
}
每次可组合项重新组合时,都会重新计算过滤后的列表。
优化之后
c
@Composable
fun ProductList(products: List<ProductUiModel>, query: String) {
val filteredProducts = remember(products, query) {
products.filter {
it.title.contains(query, ignoreCase = true)
}
}
LazyColumn {
items(
items = filteredProducts,
key = { it.id }
) { product ->
ProductCard(product)
}
}
}
详细说明
remember 会在多次重组之间存储结果,直到其中一个键发生变化。在这里,过滤操作只在产品或查询变化时运行。
同时也要注意这个关键key。稳定的item key有助于 Compose 在列表中保持项的标识。
第 4 节:错误 3------直接计算派生状态
优化之前
c
@Composable
fun CartSummary(items: List<CartItem>) {
val total = items.sumOf { it.price * it.quantity }
val hasDiscount = total > 1000
Text("Total: $total")
if (hasDiscount) {
Text("Discount unlocked")
}
}
优化之后
c
@Composable
fun CartSummary(items: List<CartItem>) {
val total by remember(items) {
derivedStateOf {
items.sumOf { it.price * it.quantity }
}
}
val hasDiscount by remember {
derivedStateOf { total > 1000 }
}
Text("Total: $total")
if (hasDiscount) {
Text("Discount unlocked")
}
}
详细说明
当某个值由其他状态计算得出,且你希望 Compose 能更高效地观察变化时,derivedStateOf 很有用。
不要在所有地方使用它。 应该在以下情况下使用:
- 这个计算结果来源于状态
- 输入经常变化
- 输出变化频率更低
- 重组的成本很高

第五部分:附加内容------避免在列表组件中使用内联 lambda
优化之前
c
@Composable
fun ProductScreen(
products: List<ProductUiModel>,
onProductClick: (String) -> Unit
) {
LazyColumn {
items(products) { product ->
ProductCard(
product = product,
onClick = { onProductClick(product.id) }
)
}
}
}
优化之后
c
@Composable
fun ProductScreen(
products: List<ProductUiModel>,
onProductClick: (String) -> Unit
) {
LazyColumn {
items(
items = products,
key = { it.id }
) { product ->
ProductCard(
product = product,
onClick = onProductClick
)
}
}
}
@Composable
fun ProductCard(
product: ProductUiModel,
onClick: (String) -> Unit
) {
Card(onClick = { onClick(product.id) }) {
Text(product.title)
}
}
最大的教训不是"避免重组"。重组是 Compose 在正常工作。真正的教训是:让你的状态保持稳定,让你的计算有目的性,不要强迫 Compose 猜测,而应该提供更准确的信息。