Deferring state reads

Jetpack Compose Senior Kotlin 2.0+ API 24+ Reviewed By Best Latest Apps

Pick the fix

Scrolling recomposes Screen on every frame. Which change keeps composition out of it?

@Composable
fun Screen(items: List<String>) {
    val listState = rememberLazyListState()
    Box {
        LazyColumn(state = listState) { items(items) { Text(it) } }
        Text("Header",
            Modifier.offset(y = (-listState.firstVisibleItemScrollOffset).dp))
    }
}
Options
Hint 1 of 1

In which phase is the offset value read?

Choose an answer, then open Show answer below to check it.

Show answer

Answer: A Use Modifier.offset { IntOffset(0, -listState.firstVisibleItemScrollOffset) }

Correct. The lambda is read in the layout phase, so composition doesn't re-run.

Why not the others?
  • B Wrap the offset in remember. remember caches one value and the header would stop moving.
  • C Use derivedStateOf for the offset. That helps when you only need a coarser value; here the exact offset changes every frame.
  • D Move the header into a LazyColumn item. That changes the layout, not where the state is read.
Indented list: Screen, LazyColumn and Header while scrolling one second (about 60 frames). Compare reading the offset in composition with the lambda modifier.

Illustrative, based on Compose and Android rules; not a device recording

Text version
Recompositions after: Scroll for one second
Composableoffset(y = ...)offset { ... }
Screen60 recompositions0: skipped
 LazyColumn0: skipped0: skipped
 Header60 recompositions0: skipped

30-second answer

Reading scroll state in a composable body makes that scope recompose on every scroll pixel. Defer the read to a later phase: use the lambda form Modifier.offset { IntOffset(0, -state.firstVisibleItemScrollOffset) } or graphicsLayer { }. Then only layout or drawing re-runs, and composition is skipped entirely.

Practise saying it · 35 s. No recording, just a timer

Explain deferring state reads.

Say your answer out loud, then check which of these points you covered:

  • Phases: composition, layout, draw
  • Reading in the body recomposes
  • Lambda modifiers read in layout or draw
  • Verify with recomposition counts

Deep dive

Compose phases run composition, then layout, then drawing. A state read inside a layout or draw lambda only invalidates that phase.

Modifier.offset(x.dp) reads during composition; Modifier.offset { } reads during layout.

drawBehind { } and graphicsLayer { alpha = ... } read during drawing, ideal for animated colours and alpha.

Pass a lambda () -> Int instead of an Int to children that only need the value in layout or draw.

Common mistakes

  • Passing a scroll offset as an Int parameter down the tree.
  • Using Modifier.alpha(animatedValue) in a hot animation when graphicsLayer { alpha = ... } would skip composition.
  • Assuming 'it's only a few composables' without measuring.

Senior take & follow-ups

Name the three phases and which modifier reads in which phase.

Follow-up questions

Think of your answer first, then open the hint.

  1. What does graphicsLayer give you?

    Show hint

    A separate layer whose properties can change in the draw phase only.

  2. How do you confirm the fix?

    Show hint

    Recomposition counts in Layout Inspector stay flat while scrolling.

Suggest a fix if something here is wrong or out of date.