Loading...
Loading...
Use when writing or reviewing Jetpack Compose code with bare local var in a @Composable, remember { mutableStateOf(...) }, mutableStateListOf/mutableStateMapOf, or @ReadOnlyComposable.
npx skill4agent add chrisbanes/skills compose-state-authoringremember { … }remember { mutableStateOf(…) }mutableStateListOfmutableStateMapOf@ReadOnlyComposablerememberCoroutineScoperememberUpdatedStatecompose-side-effectsrememberLazyListStaterememberScrollStatecompose-state-deferred-readsFocusRequestercompose-focus-navigation@Composablevarremember@ReadOnlyComposablevar x = …@Composable funColumn { var x = … }@Composable fun@Composable get()@ReadOnlyComposableTextBoxColumnremembervarvar// ❌ BAD — counter resets on every recomposition; clicks never update the UI
@Composable
fun Counter() {
var count = 0
Button(onClick = { count++ }) { Text("$count") }
}
// ❌ ALSO BAD — same rule applies inside composable content lambdas
@Composable
fun Wrapper() {
Row {
var count = 0 // Row's content lambda is @Composable too
// …
}
}// ✅ GOOD — `remember` survives recomposition, `mutableStateOf` triggers it
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) { Text("$count") }
}remember { … }mutableStateOf(…)mutableStateListOfmutableStateMapOfrememberremember { mutableStateOf(mutableListOf<X>()) }list.add(x)MutableList.addstate = state + xremember { … }varval builder = remember { mutableListOf<X>().apply { var n = 0; … } }@ComposableonClick = { var a = 0; … }() -> Unit@Composable@ReadOnlyComposable@ReadOnlyComposableTextBoxrememberMaterialTheme.colorSchemeLocalDensity.current@ReadOnlyComposable@ReadOnlyComposableLocalFoo.current// ✅ GOOD — only reads composition locals, no layout, no remember
@Composable
@ReadOnlyComposable
fun appSpacing(): Dp = LocalDimensions.current.spacing
// ✅ GOOD — composable property getter; same rule
val accent: Color
@Composable @ReadOnlyComposable
get() = MaterialTheme.colorScheme.tertiary// ❌ BAD — annotated read-only but lays out a Box; contract violated
@Composable
@ReadOnlyComposable
fun Header(): Int {
Box {} // ← non-read-only composable call
return 42
}
// ❌ BAD — calls a normal composable from a read-only one
@Composable
@ReadOnlyComposable
fun computed(): Int = nonReadOnlyHelper()@ReadOnlyComposableBoxColumnRowLazyColumnTextandroidx.compose.foundation.layoutandroidx.compose.material*LaunchedEffectDisposableEffectSideEffectproduceStateremember { … }@Composablecontent()@ReadOnlyComposableLocal*.current@ReadOnlyComposableoverride fun@ReadOnlyComposableLaunchedEffectDisposableEffectSideEffectrememberCoroutineScoperememberUpdatedStatesnapshotFlowcompose-side-effectsFocusRequestercompose-focus-navigationrequestFocuscompose-side-effectsrememberUpdatedStateremember { mutableStateOf(...) }| Symptom | Diagnosis | Fix |
|---|---|---|
| Not recomposition-safe (§1) | |
| Same — content lambdas are | Same fix |
| Mutation bypasses State setter | Use |
| Could be | Add |
| Contract violation (§2) | Remove |
composeTestRule.setContent { … }produceStateLaunchedEffectderivedStateOfoverride| Thought | Reality |
|---|---|
"It's a small composable, the bare | Recomposition can fire at any time. The reset is non-deterministic by design — and a single bug report later. |
"I'll add | "Simple" isn't the criterion. "Makes only read-only calls" is. |
"I always reach for | Use |
"I'll just | A |
"The override needs | If the base isn't |