我通过3个小改动将Compose重组减少了78%

起初,屏幕看起来很正常。没有崩溃。没有 ANR(应用程序无响应)。也没有明显的帧丢失。但是,每次我滚动、输入或更改滤镜时,我总感觉有些不对劲------就像 UI 在做更多的工作。我打开 Compose 布局检查器,期望找到一个坏的 Composable 组件。但实际上,我发现整个屏幕一直在悄悄地重新组合,这都是因为三个微小的错误,在代码审查中看起来完全无害。

核心要点

本文介绍了在 Jetpack Compose 中不必要的重组现象,以及如何通过使用稳定模型、 rememberderivedStateOf 来减少重组。

读者将学到什么

  • 为什么重组本身不是坏事
  • 不稳定的参数如何导致不必要的重组
  • 为什么在可组合项中创建对象可能是昂贵的
  • 何时使用 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 猜测,而应该提供更准确的信息。

相关推荐
v_348360876229 分钟前
Android native端 堆栈打印
android·算法
一笑的小酒馆1 小时前
AndroidKMP之导航和WebView
android
2501_932750264 小时前
Android BottomNavigationView 入门介绍
android
Godikov6 小时前
硬件异步事件下的业务一致性:智能柜领取/归还流程的竞态设计
android
Godikov6 小时前
不用 Netty:Android 手写 TCP 客户端实战——自定义协议帧、粘包处理与可靠重连
android
事圆则缓6 小时前
Java 面向对象如何落到 Android View 体系
android
iPad协议个微协议7 小时前
# 微信自动营销系统如何基于 WechatApi 做事件驱动设计
android·微信·企业微信·个人开发·wechatapi·微信个人号开发
AirDroid_cn7 小时前
隐私沙盒:第三方应用读取剪切板内容如何拦截?
android·gitee
2501_915918419 小时前
怎么把 Python 写的 Flet 应用打包成 iOS App 并上架 App Store?
android·ios·小程序·https·uni-app·iphone·webview
邪修king11 小时前
Re:Linux系统篇(二十六):文件系统(二):Ext 文件系统底层详解:从 inode、块组到软硬链接,结合 Windows 讲透文件管理本质
android·java·linux