SuperAPI AI 工具集

compose-state-deferred-reads

前端与建站

查看项目

更新于 10-16 05:38

技能介绍

用途

该技能用于解决 Jetpack Compose 中帧率级状态(如滚动偏移、动画值、拖拽位置)在组合阶段被读取而导致的性能问题。其核心原则是:状态读取会使读取它的阶段失效。若在 composable 函数体内读取 State<T>,变化会触发重组;若在布局或绘制阶段读取,则只使布局或绘制失效。帧率级状态通常应放在布局/绘制阶段,而非组合阶段。修复方式是结构性的:保留 State<T> 或提供者 lambda,在布局/绘制回调内读取值。

主要能力

1. 优先使用块形式修饰符

多个修饰符有值形式和块形式。值形式接收已在组合中读取的值;块形式可在布局或绘制阶段读取。

示例(前):

@Composable
fun SelectionPill(selectedIndex: Int) {
    val offsetX by animateDpAsState(120.dp * selectedIndex)
    Box(Modifier.offset(x = offsetX))
}

示例(后):

@Composable
fun SelectionPill(selectedIndex: Int) {
    val offsetX = animateDpAsState(120.dp * selectedIndex)
    Box(
        Modifier.offset {
            IntOffset(offsetX.value.roundToPx(), 0)
        },
    )
}

常见替换对照:

组合阶段读取 延迟读取
Modifier.offset(x = animatedX) Modifier.offset { IntOffset(animatedX.value.roundToPx(), 0) }
Modifier.graphicsLayer(translationY = y) Modifier.graphicsLayer { translationY = yProvider() }
val radius by animateFloatAsState(...); drawBehind { drawCircle(radius = radius) } val radius = animateFloatAsState(...); drawBehind { drawCircle(radius = radius.value) }

drawBehind 块本身已是绘制阶段;关键在于 State.value 的读取也发生在该块内。

2. 跨 composable 边界传递提供者

若快速变化的值需要跨越 composable 边界,应传递提供者 lambda 而非快照值。

示例(前):

@Composable
fun HomeScreen() {
    val listState = rememberLazyListState()
    LazyColumn(state = listState) {
        item { HeroImage(scrollOffset = listState.firstVisibleItemScrollOffset) }
    }
}

@Composable
fun HeroImage(scrollOffset: Int, modifier: Modifier = Modifier) {
    AsyncImage(
        model = "...",
        modifier = modifier.graphicsLayer(translationY = -scrollOffset / 2f),
    )
}

示例(后):

@Composable
fun HomeScreen() {
    val listState = rememberLazyListState()
    LazyColumn(state = listState) {
        item {
            HeroImage(
                scrollOffsetProvider = {
                    if (listState.firstVisibleItemIndex == 0) {
                        listState.firstVisibleItemScrollOffset
                    } else {
                        0
                    }
                },
            )
        }
    }
}

@Composable
fun HeroImage(scrollOffsetProvider: () -> Int, modifier: Modifier = Modifier) {
    AsyncImage(
        model = "...",
        modifier = modifier.graphicsLayer {
            translationY = -scrollOffsetProvider() / 2f
        },
    )
}

当有助于明确延迟读取契约时,提供者参数应以 Provider 为后缀。

3. 其他布局/绘制读取位置

状态读取也可延迟到以下位置:

  • Modifier.layout { measurable, constraints -> ... }
  • 自定义 Alignment.align(...)
  • drawWithContent、drawBehind 及其他绘制修饰符
  • 块形式图层/布局修饰符,如 graphicsLayer { ... } 和 offset { ... }

当状态改变的是某物的放置或绘制位置时使用这些方式。若状态决定存在哪些 composable,则应保留在组合阶段。

快速参考

症状 诊断 修复
val x by animateFloatAsState(...) 后接 Modifier.offset(...) by 在组合阶段读取 保留 State<Float>,在 offset {} 中读取 .value
Modifier.graphicsLayer(translationY = animatedY) 属性参数形式使用组合阶段值 改用 graphicsLayer { translationY = ... }
Child(scrollOffset = listState.firstVisibleItemScrollOffset) 快速变化值跨越边界 改用 Child(scrollOffsetProvider = { ... })
绘制块仍每帧重组 值在绘制块之前被读取 将 State.value 读取移入绘制块内
状态在不同 UI 分支间选择 组合阶段决策 保留在组合阶段读取

适用场景

  • val x by animate*AsState(...) 被传给 Modifier.offset(x = ...)、Modifier.size(...)、Modifier.graphicsLayer(...) 或其他值形式修饰符。
  • LazyListState.firstVisibleItemScrollOffset、ScrollState.value、Animatable.value 或手势状态在 composable 函数体内被读取。
  • composable 接收 scrollOffset: Int、progress: Float、dragOffset: Offset 或类似帧率值。
  • 即使数据稳定,滚动、动画或手势期间重组计数仍上升。

限制

以下情况不应应用该技能:

  • 状态控制发出哪些 composable。
  • 动画是一次性的、开销低,且清晰性更重要。
  • 编写测试时直接值断言更简单。
  • 运行时证据表明重组并非瓶颈。

相关技能

  • compose-state-holder-ui-split:跨边界传递提供者/lambda 时,状态持有者与纯 UI 拆分的适用场景。
  • compose-stability-diagnostics:参数稳定性与编译器报告。
  • compose-modifier-and-layout-style:子 composable 需要普通 modifier 参数,调用方才能将视觉读取移入修饰符。