用途
该技能用于解决 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 参数,调用方才能将视觉读取移入修饰符。