Skip to content
Development
Skill

/compose-state-deferred-reads

Use when Jetpack Compose code reads scroll, animation, gesture, or other frame-rate State in composition, passes changing values across composable boundaries, or uses value-form layout/draw modifiers. Technique-layer skill — complements the codebase-specific compose-expert.

From plugin
amethyst
1.6k30 skills3 commands
Install
$ npx -y skills add vitorpamplona/amethyst --skill compose-state-deferred-reads --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/compose-state-deferred-reads

Context preview

The summary Claude sees to decide when to auto-load this skill.

Use when Jetpack Compose code reads scroll, animation, gesture, or other frame-rate State in composition, passes changing values across composable boundaries, or uses value-form layout/draw modifiers. Technique-layer skill — complements the codebase-specific compose-expert.

SKILL.md

compose-state-deferred-reads.SKILL.md
name: compose-state-deferred-reads
description: Use when Jetpack Compose code reads scroll, animation, gesture, or other frame-rate State in composition, passes changing values across composable boundaries, or uses value-form layout/draw modifiers. Technique-layer skill — complements the codebase-specific compose-expert.

Compose state deferred reads

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.

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.

1. Prefer block-form modifiers

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.

// 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 read | Deferred 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.

2. Pass providers across composable boundaries

If the fast-changing value would cross a composable boundary, pass a provider lambda instead of a snapshot value:

// 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.

3. Other layout/draw read sites

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.

Quick reference

| Symptom | Diagnosis | Fix | |---|---|---| | `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 values | Use `graphicsLayer { translationY = ... }` | | `Child(scrollOffset = listState.firstVisibleItemScrollOffset)` | Fast-changing value crosses boundary | `Child(scrollOffsetProvider = { ... })` | | Draw block still recomposes every frame | Value was read before draw block | Move the `State.value` read inside the draw block | | State chooses between different UI branches | Composition decision | Keep the read in composition |

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`](../compose-state-holder-ui-split/SKILL.md) - where state-holder vs plain UI split applies when passing providers/lambdas across boundaries.
  • [`compose-stability-diagnostics`](../compose-stability-diagnostics/SKILL.md) - parameter stability and compiler reports.
  • [`compose-modifier-and-layout-style`](../comp
Read more
Ships withamethyst

Nostr client for Android

Get the whole plugin
Stats
1,599
Stars
221
Forks
Active
Maintenance
Kotlin
Language
MIT
License
8h ago
Last commit
3y ago
Created

Repo: vitorpamplona/amethyst

Other skills on amethyst.