compose-state-authoring
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCompose state authoring
Compose 状态编写规范
Not every belongs here. This skill covers local UI state (, / ) and . Other remembered APIs live in focused skills:
remember { … }remember { mutableStateOf(…) }mutableStateListOfmutableStateMapOf@ReadOnlyComposable- /
rememberCoroutineScope→rememberUpdatedStatecompose-side-effects - /
rememberLazyListStateused for frame-rate reads →rememberScrollStatecompose-state-deferred-reads - Focus navigation, focus state, ownership, behavior →
FocusRequestercompose-focus-navigation
并非所有都属于本技能范畴。本技能涵盖本地UI状态(、 / )以及****。其他带remember的API对应专属技能:
remember { … }remember { mutableStateOf(…) }mutableStateListOfmutableStateMapOf@ReadOnlyComposable- /
rememberCoroutineScope→rememberUpdatedStatecompose-side-effects - /
rememberLazyListState用于帧率读取 →rememberScrollStatecompose-state-deferred-reads - 焦点导航、焦点状态、所有权与行为 →
FocusRequestercompose-focus-navigation
Core principle
核心原则
A is a function the runtime re-runs whenever its inputs change. Writing local state correctly comes down to two questions:
@Composable- Mutable local state — does my survive recomposition and trigger it? If not, it silently resets on every recompose and writes are invisible.
var - What kind of composable is this? — do I mutate composition (place layout nodes, allocate slots, ) or only read it? If only read,
rememberlets the runtime skip work.@ReadOnlyComposable
Get either wrong and the symptoms are subtle: state that vanishes or optimizations that don't apply.
@Composable- 可变本地状态 —— 我的能否在重组后保留值并触发重组?如果不能,它会在每次重组时无声重置,修改操作也无法在UI中体现。
var - Composable类型 —— 我是否会修改组合(放置布局节点、分配插槽、调用),还是仅读取组合?如果仅读取,
remember可让运行时跳过不必要的工作。@ReadOnlyComposable
任何一个问题处理不当都会导致细微的问题:状态莫名消失或优化不生效。
When to use this skill
何时使用本技能
You're writing or reviewing Compose code and you see any of these:
- inside a
var x = …or any composable lambda (@Composable fun)Column { var x = … } - A (or
@Composable funproperty accessor) whose body never lays anything out@Composable get() - on a function that calls
@ReadOnlyComposable,Text,Box,Column, …remember - A composable whose visible state mysteriously resets on rotation, theme change, or recomposition
当你编写或审查Compose代码时,遇到以下场景:
- 或任意Composable lambda(如
@Composable fun)内部出现Column { var x = … }var x = … - (或
@Composable fun属性访问器)的函数体从未进行任何布局操作@Composable get() - 调用、
Text、Box、Column等函数的标记了remember的函数@ReadOnlyComposable - 可见状态会在屏幕旋转、主题变更或重组时莫名重置的Composable
1. var
in a composable must be State-backed
var1. Composable中的var必须基于State实现
Recomposition re-executes the composable from the top. A local is re-initialized on every pass — last recompose's value is gone, and writing to it doesn't tell the runtime to recompose.
varkotlin
// ❌ 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
// …
}
}kotlin
// ✅ GOOD — `remember` survives recomposition, `mutableStateOf` triggers it
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) { Text("$count") }
}Two pieces and both matter:
- — survives recomposition. Without it the value is re-created each time.
remember { … } - — triggers recomposition. Without it, mutations are invisible to the runtime.
mutableStateOf(…)
For collections, prefer / (also -ed). They emit Snapshot reads on every read and Snapshot writes on every mutation. A followed by will not recompose, because doesn't go through the State setter — you'd have to replace the value ().
mutableStateListOfmutableStateMapOfrememberremember { mutableStateOf(mutableListOf<X>()) }list.add(x)MutableList.addstate = state + x重组会从头重新执行Composable。本地会在每次执行时重新初始化——上一次重组的值会丢失,修改它也不会通知运行时触发重组。
varkotlin
// ❌ 错误——计数器在每次重组时重置;点击操作无法更新UI
@Composable
fun Counter() {
var count = 0
Button(onClick = { count++ }) { Text("$count") }
}
// ❌ 同样错误——该规则也适用于Composable内容lambda
@Composable
fun Wrapper() {
Row {
var count = 0 // Row的内容lambda也是@Composable
// …
}
}kotlin
// ✅ 正确——`remember`确保值在重组后保留,`mutableStateOf`触发重组
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) { Text("$count") }
}两个部分都至关重要:
- —— 在重组后保留值。没有它,值会在每次重组时重新创建。
remember { … } - —— 触发重组。没有它,修改操作对运行时是不可见的。
mutableStateOf(…)
对于集合类型,优先使用 / (同样需要配合)。它们会在每次读取和修改时触发Snapshot读写。如果使用后调用,不会触发重组,因为不会经过State的setter——你必须替换整个值()。
mutableStateListOfmutableStateMapOfrememberremember { mutableStateOf(mutableListOf<X>()) }list.add(x)MutableList.addstate = state + xWhen this rule does NOT apply
本规则不适用的场景
- Inside 's producer block. That runs once per key change, not on every recompose. A local
remember { … }there is fine:var.val builder = remember { mutableListOf<X>().apply { var n = 0; … } } - In non-lambdas passed out of a composable.
@Composableis a plainonClick = { var a = 0; … }. Local vars there are normal Kotlin.() -> Unit - In plain (non-) helper functions. Only composable scopes are affected.
@Composable
- 的生产者块内部。该代码块仅在key变化时执行一次,而非每次重组。这里的本地var是安全的:
remember { … }。val builder = remember { mutableListOf<X>().apply { var n = 0; … } } - 传递到Composable外部的非lambda。
@Composable是普通的onClick = { var a = 0; … },其中的本地var是正常的Kotlin变量。() -> Unit - 普通(非)辅助函数。仅Composable作用域受该规则影响。
@Composable
2. The @ReadOnlyComposable
contract
@ReadOnlyComposable2. @ReadOnlyComposable
契约
@ReadOnlyComposable@ReadOnlyComposableTextBoxrememberMaterialTheme.colorSchemeLocalDensity.currentThe contract is bidirectional:
- Add when every composable call your body makes is itself
@ReadOnlyComposable(or there are no composable calls at all — for example a function that only reads@ReadOnlyComposableand returns a value).LocalFoo.current - Don't add it if you call any non-read-only composable. The optimization assumes you don't participate in composition; violating that produces incorrect recomposition behaviour for callers.
kotlin
// ✅ 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.tertiarykotlin
// ❌ 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()@ReadOnlyComposableTextBoxrememberMaterialTheme.colorSchemeLocalDensity.current该契约是双向的:
- 添加:当函数体调用的所有Composable本身都是
@ReadOnlyComposable(或没有调用任何Composable——例如仅读取@ReadOnlyComposable并返回值的函数)时。LocalFoo.current - 不要添加:如果调用了任何非只读的Composable。该优化假设你不参与组合;违反契约会导致调用方出现不正确的重组行为。
@ReadOnlyComposable
kotlin
// ✅ 正确——仅读取组合本地变量,无布局或remember调用
@Composable
@ReadOnlyComposable
fun appSpacing(): Dp = LocalDimensions.current.spacing
// ✅ 正确——Composable属性getter;规则相同
val accent: Color
@Composable @ReadOnlyComposable
get() = MaterialTheme.colorScheme.tertiarykotlin
// ❌ 错误——标记为只读但布局了Box;违反契约
@Composable
@ReadOnlyComposable
fun Header(): Int {
Box {} // ← 非只读Composable调用
return 42
}
// ❌ 错误——从只读Composable调用普通Composable
@Composable
@ReadOnlyComposable
fun computed(): Int = nonReadOnlyHelper()Heuristic for "should I add it"
判断是否添加的启发式规则
If the body contains any of these, do not add :
@ReadOnlyComposable- A layout call: ,
Box,Column,Row,LazyColumn, anything fromTextorandroidx.compose.foundation.layout.androidx.compose.material* - A side-effect call: ,
LaunchedEffect,DisposableEffect,SideEffect.produceState - — positional memoization is composition state.
remember { … } - A lambda invocation (
@Composable).content() - An invocation of a non-composable function.
@ReadOnlyComposable
If the body is only reading , calling other functions, or doing pure computation, add it.
Local*.current@ReadOnlyComposable如果函数体包含以下任意内容,请勿添加:
@ReadOnlyComposable- 布局调用:、
Box、Column、Row、LazyColumn,以及Text或androidx.compose.foundation.layout中的任意函数。androidx.compose.material* - 副作用调用:、
LaunchedEffect、DisposableEffect、SideEffect。produceState - —— 位置记忆属于组合状态。
remember { … } - lambda调用(
@Composable)。content() - 调用非的Composable函数。
@ReadOnlyComposable
如果函数体仅读取、调用其他函数或执行纯计算,请添加该注解。
Local*.current@ReadOnlyComposableWhen this rule does NOT apply
本规则不适用的场景
- declarations. The annotation is part of the contract; if the base isn't
override fun, you can't make an override one. Refactor the base, or accept the override pays the group-creation cost.@ReadOnlyComposable - Abstract declarations. No body to check.
- 声明。注解是契约的一部分;如果基函数不是
override fun,你无法将重写函数标记为该注解。请重构基函数,或接受重写函数需要承担分组创建的开销。@ReadOnlyComposable - 抽象声明。没有函数体可供检查。
Related: side effects live in their own skill
相关:副作用属于专属技能范畴
If a composable needs , , , , , , snackbar/navigation handling, analytics, or Flow collection, use .
LaunchedEffectDisposableEffectSideEffectrememberCoroutineScoperememberUpdatedStatesnapshotFlowcompose-side-effectsFocus splits by question: navigation, focus state, ownership, behavior → ; when to call imperative (effect timing, lifecycle, keys, API choice) → .
FocusRequestercompose-focus-navigationrequestFocuscompose-side-effectsThis skill is about authoring Compose state correctly. is effect capture state, not a general replacement for . Side effects have separate lifecycle and keying rules, and keeping them in one focused skill avoids two sources of truth.
rememberUpdatedStateremember { mutableStateOf(...) }如果Composable需要、、、、、、 snackbar/导航处理、分析或Flow收集,请使用。
LaunchedEffectDisposableEffectSideEffectrememberCoroutineScoperememberUpdatedStatesnapshotFlowcompose-side-effects关注点按问题拆分:导航、焦点状态、所有权与行为 → ;何时调用命令式(副作用时机、生命周期、key、API选择)→ 。
FocusRequestercompose-focus-navigationrequestFocuscompose-side-effects本技能专注于正确编写Compose状态。是副作用捕获状态,并非的通用替代方案。副作用有独立的生命周期和key规则,将它们放在专属技能中可避免出现两套规则。
rememberUpdatedStateremember { mutableStateOf(...) }Quick reference
快速参考
| 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 |
| 症状 | 诊断 | 修复方案 |
|---|---|---|
| 不符合重组安全要求(§1) | 改为 |
| 同上——内容lambda也是 | 相同修复方案 |
使用 | 修改操作绕过了State的setter | 使用 |
| 可标记为 | 在 |
标记 | 违反契约(§2) | 移除 |
When NOT to apply
请勿应用的场景
- Tests with follow the same rules — they're production composables.
composeTestRule.setContent { … } - has its own producer block that runs in a coroutine; you don't need
produceStateinside it.LaunchedEffect - has its own concerns around stability and equality — out of scope here; it's about preventing recomposition, not authoring state.
derivedStateOf - s of read-only-composable declarations: the annotation is fixed by the base; you can't add or remove it locally.
override
- 使用的测试代码遵循相同规则——它们属于生产级Composable。
composeTestRule.setContent { … } - ****有自己的协程生产者块;无需在其内部使用
produceState。LaunchedEffect - ****涉及稳定性和相等性的相关问题——超出本技能范畴;它专注于避免重组,而非状态编写。
derivedStateOf - 重写只读Composable声明:注解由基函数决定;无法在本地添加或移除。
Red flags during review
代码审查中的危险信号
| 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 |
| 错误想法 | 实际情况 |
|---|---|
| "这是个小型Composable,裸var没问题" | 重组可能随时触发。状态重置是设计上的非确定性问题——后续会出现Bug报告。 |
"这个函数看起来简单,我要加 | "简单"不是判断标准。"仅调用只读函数"才是。 |
"我总是用 | 请使用 |
"我直接在remember的列表上调用 | |
"重写函数需要 | 如果基函数不是 |