/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.
$ npx -y skills add vitorpamplona/amethyst --skill compose-state-deferred-reads --agent claude-codeHow 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.mdname: 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
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
Other skills on amethyst.
- /account-state
Account state and in-memory event store patterns in Amethyst. Use when working with `Account.kt` (per-user state objects — `kind3FollowList`, `nip65RelayList`, `muteList`, `bookmarkState`, each exposing a `.flow` StateFlow), `LocalCache` (the object-level event store backed by
Open skill - /amy-expert
Patterns for extending `amy`, the Amethyst CLI in `cli/`. Use when adding an `amy <verb>` command, touching files under `cli/src/main/kotlin/…/cli/`, wiring a new subcommand into `Main.kt`, writing an interop test script that drives Amy, or extracting logic out of `amethyst/`
Open skill - /android-expert
Android platform patterns for the `amethyst/` module. Use when working with (1) Android navigation (Navigation Compose, type-safe routes, bottom nav), (2) runtime permissions (camera, notifications, biometrics), (3) platform APIs (Intent, Context, Activity, ContentResolver), (4)
Open skill - /auth-signers
Signer abstraction patterns in Amethyst. Use when working with event signing, choosing between a local keypair (`NostrSignerInternal`), a remote NIP-46 bunker signer (`NostrSignerRemote`), or a NIP-55 Android external-app signer (`NostrSignerExternal`). Covers the abstract
Open skill - /compose-expert
Advanced Compose Multiplatform UI patterns for shared composables. Use when working with visual UI components, state management patterns (remember, derivedStateOf, produceState), recomposition optimization (@Stable/@Immutable visual usage), Material3 theming, custom ImageVector
Open skill - /compose-modifier-and-layout-style
Use when writing or reviewing Jetpack Compose layout APIs, modifier parameters, modifier chain construction, hardcoded root layout decisions, or layout wrappers around a single conditional. Technique-layer skill — complements the codebase-specific compose-expert.
Open skill

