compose-state-deferred-reads

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Compose state deferred reads

Compose 状态延迟读取

Core principle

核心原则

State reads invalidate the phase that reads them. If a
State<T>
is read in a composable body, changes invalidate composition. If it is read in layout or draw, changes can invalidate only layout or draw. Frame-rate state such as scroll offsets, animations, and drag positions usually belongs in layout/draw, not composition.
The fix is structural: keep the
State<T>
or a provider lambda, and read the value inside a layout/draw callback.
状态读取会使读取该状态的阶段失效。如果在可组合项主体中读取
State<T>
,状态变化会使组合阶段失效。如果在布局或绘制阶段读取,状态变化只会使布局或绘制阶段失效。诸如滚动偏移、动画和拖拽位置这类帧率相关的状态通常属于布局/绘制阶段,而非组合阶段。
修复方法是结构化调整:保留
State<T>
或提供者lambda,在布局/绘制回调内部读取值。

When to use this skill

何时使用本技巧

  • val x by animate*AsState(...)
    is passed to
    Modifier.offset(x = ...)
    ,
    Modifier.size(...)
    ,
    Modifier.graphicsLayer(...)
    , or another value-form modifier.
  • LazyListState.firstVisibleItemScrollOffset
    ,
    ScrollState.value
    ,
    Animatable.value
    , or gesture state is read in a composable body.
  • A composable takes
    scrollOffset: Int
    ,
    progress: Float
    ,
    dragOffset: Offset
    , or similar frame-rate values.
  • Recomposition counters climb during scroll, animation, or gestures even when data is stable.
  • val x by animate*AsState(...)
    被传递给
    Modifier.offset(x = ...)
    Modifier.size(...)
    Modifier.graphicsLayer(...)
    或其他值形式的修饰符。
  • 在可组合项主体中读取
    LazyListState.firstVisibleItemScrollOffset
    ScrollState.value
    Animatable.value
    或手势状态。
  • 可组合项接收
    scrollOffset: Int
    progress: Float
    dragOffset: Offset
    或类似的帧率相关值。
  • 滚动、动画或手势过程中,即使数据稳定,重组计数器仍持续攀升。

1. Prefer block-form modifiers

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

Several modifiers have value forms and block forms. The value form receives values already read in composition; the block form can read during layout or draw.
kotlin
// Before: animated value read in composition by the `by` delegate
@Composable
fun SelectionPill(selectedIndex: Int) {
    val offsetX by animateDpAsState(120.dp * selectedIndex)
    Box(Modifier.offset(x = offsetX))
}

// After: State is kept, value is read in the layout-phase offset block
@Composable
fun SelectionPill(selectedIndex: Int) {
    val offsetX = animateDpAsState(120.dp * selectedIndex)
    Box(
        Modifier.offset {
            IntOffset(offsetX.value.roundToPx(), 0)
        },
    )
}
Common replacements:
Composition readDeferred read
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) }
The
drawBehind
block is already draw-phase; the important part is that the
State.value
read also happens inside that block.
部分修饰符同时提供值形式和块形式。值形式接收已在组合阶段读取的值;块形式可在布局或绘制阶段读取值。
kotlin
// Before: animated value read in composition by the `by` delegate
@Composable
fun SelectionPill(selectedIndex: Int) {
    val offsetX by animateDpAsState(120.dp * selectedIndex)
    Box(Modifier.offset(x = offsetX))
}

// After: State is kept, value is read in the layout-phase offset block
@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. Pass providers across composable boundaries

2. 跨可组合项边界传递提供者

If the fast-changing value would cross a composable boundary, pass a provider lambda instead of a snapshot value:
kotlin
// Before: HomeScreen reads scroll offset in composition and passes the value down
@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),
    )
}

// After: the only read happens inside graphicsLayer
@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
        },
    )
}
Suffix provider parameters with
Provider
when that clarifies the deferred-read contract.
如果快速变化的值需要跨可组合项边界传递,应传递提供者lambda而非快照值:
kotlin
// Before: HomeScreen reads scroll offset in composition and passes the value down
@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),
    )
}

// After: the only read happens inside graphicsLayer
@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. Other layout/draw read sites

3. 其他布局/绘制读取场景

State reads can also be deferred inside:
  • Modifier.layout { measurable, constraints -> ... }
  • Custom
    Alignment.align(...)
  • drawWithContent
    ,
    drawBehind
    , and other draw modifiers
  • Block-form layer/layout modifiers such as
    graphicsLayer { ... }
    and
    offset { ... }
Use these when the state changes where something is placed or painted. If the state decides which composables exist, it belongs in composition.
状态读取也可在以下场景中延迟:
  • Modifier.layout { measurable, constraints -> ... }
  • 自定义
    Alignment.align(...)
  • drawWithContent
    drawBehind
    及其他绘制修饰符
  • 块形式的图层/布局修饰符,如
    graphicsLayer { ... }
    offset { ... }
当状态会改变元素的位置或绘制方式时,使用上述方式。如果状态决定哪些可组合项存在,则应在组合阶段读取。

Quick reference

快速参考

SymptomDiagnosisFix
val x by animateFloatAsState(...)
then
Modifier.offset(...)
by
reads in composition
Keep
State<Float>
and read
.value
in
offset {}
Modifier.graphicsLayer(translationY = animatedY)
Property-argument form uses composition valuesUse
graphicsLayer { translationY = ... }
Child(scrollOffset = listState.firstVisibleItemScrollOffset)
Fast-changing value crosses boundary
Child(scrollOffsetProvider = { ... })
Draw block still recomposes every frameValue was read before draw blockMove the
State.value
read inside the draw block
State chooses between different UI branchesComposition decisionKeep the read in composition
症状诊断修复方案
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分支的显示组合阶段决策保留组合阶段的读取操作

When NOT to apply

何时不适用

  • The state controls which composables are emitted.
  • The animation is one-shot, cheap, and clarity wins.
  • You are writing tests where direct value assertions are simpler.
  • Runtime evidence shows recomposition is not the bottleneck.
  • 状态控制哪些可组合项被渲染。
  • 动画是一次性的、开销低,且代码清晰度优先。
  • 编写测试时,直接断言值更简单。
  • 运行时证据表明重组并非性能瓶颈。

Related

相关内容

  • compose-state-holder-ui-split
    - where state-holder vs plain UI split applies when passing providers/lambdas across boundaries.
  • compose-stability-diagnostics
    - parameter stability and compiler reports.
  • compose-modifier-and-layout-style
    - child composables need a normal
    modifier
    parameter before callers can move visual reads into modifiers.
  • compose-state-holder-ui-split
    - 跨边界传递提供者/lambda时,状态持有者与纯UI拆分的适用场景。
  • compose-stability-diagnostics
    - 参数稳定性与编译器报告。
  • compose-modifier-and-layout-style
    - 子可组合项需要常规
    modifier
    参数,以便调用者将视觉读取操作移至修饰符中。