compose-state-authoring

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Compose state authoring

Compose 状态编写规范

Not every
remember { … }
belongs here. This skill covers local UI state (
remember { mutableStateOf(…) }
,
mutableStateListOf
/
mutableStateMapOf
) and
@ReadOnlyComposable
. Other remembered APIs live in focused skills:
  • rememberCoroutineScope
    /
    rememberUpdatedState
    compose-side-effects
  • rememberLazyListState
    /
    rememberScrollState
    used for frame-rate reads →
    compose-state-deferred-reads
  • Focus navigation, focus state,
    FocusRequester
    ownership, behavior
    compose-focus-navigation
并非所有
remember { … }
都属于本技能范畴。本技能涵盖本地UI状态
remember { mutableStateOf(…) }
mutableStateListOf
/
mutableStateMapOf
)以及**
@ReadOnlyComposable
**。其他带remember的API对应专属技能:
  • rememberCoroutineScope
    /
    rememberUpdatedState
    compose-side-effects
  • rememberLazyListState
    /
    rememberScrollState
    用于帧率读取 →
    compose-state-deferred-reads
  • 焦点导航、焦点状态、
    FocusRequester
    所有权与行为
    compose-focus-navigation

Core principle

核心原则

A
@Composable
is a function the runtime re-runs whenever its inputs change. Writing local state correctly comes down to two questions:
  1. Mutable local state — does my
    var
    survive recomposition and trigger it? If not, it silently resets on every recompose and writes are invisible.
  2. What kind of composable is this? — do I mutate composition (place layout nodes, allocate slots,
    remember
    ) or only read it? If only read,
    @ReadOnlyComposable
    lets the runtime skip work.
Get either wrong and the symptoms are subtle: state that vanishes or optimizations that don't apply.
@Composable
是一种运行时会在输入变化时重新执行的函数。正确编写本地状态需关注两个问题:
  1. 可变本地状态 —— 我的
    var
    能否在重组后保留值并触发重组?如果不能,它会在每次重组时无声重置,修改操作也无法在UI中体现。
  2. Composable类型 —— 我是否会修改组合(放置布局节点、分配插槽、调用
    remember
    ),还是仅读取组合?如果仅读取,
    @ReadOnlyComposable
    可让运行时跳过不必要的工作。
任何一个问题处理不当都会导致细微的问题:状态莫名消失或优化不生效。

When to use this skill

何时使用本技能

You're writing or reviewing Compose code and you see any of these:
  • var x = …
    inside a
    @Composable fun
    or any composable lambda (
    Column { var x = … }
    )
  • A
    @Composable fun
    (or
    @Composable get()
    property accessor) whose body never lays anything out
  • @ReadOnlyComposable
    on a function that calls
    Text
    ,
    Box
    ,
    Column
    ,
    remember
    , …
  • A composable whose visible state mysteriously resets on rotation, theme change, or recomposition
当你编写或审查Compose代码时,遇到以下场景:
  • @Composable fun
    或任意Composable lambda(如
    Column { var x = … }
    )内部出现
    var x = …
  • @Composable fun
    (或
    @Composable get()
    属性访问器)的函数体从未进行任何布局操作
  • 调用
    Text
    Box
    Column
    remember
    等函数的标记了
    @ReadOnlyComposable
    的函数
  • 可见状态会在屏幕旋转、主题变更或重组时莫名重置的Composable

1.
var
in a composable must be State-backed

1. Composable中的var必须基于State实现

Recomposition re-executes the composable from the top. A local
var
is re-initialized on every pass — last recompose's value is gone, and writing to it doesn't tell the runtime to recompose.
kotlin
// ❌ 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:
  • remember { … }
    survives recomposition. Without it the value is re-created each time.
  • mutableStateOf(…)
    triggers recomposition. Without it, mutations are invisible to the runtime.
For collections, prefer
mutableStateListOf
/
mutableStateMapOf
(also
remember
-ed). They emit Snapshot reads on every read and Snapshot writes on every mutation. A
remember { mutableStateOf(mutableListOf<X>()) }
followed by
list.add(x)
will not recompose, because
MutableList.add
doesn't go through the State setter — you'd have to replace the value (
state = state + x
).
重组会从头重新执行Composable。本地
var
会在每次执行时重新初始化——上一次重组的值会丢失,修改它也不会通知运行时触发重组。
kotlin
// ❌ 错误——计数器在每次重组时重置;点击操作无法更新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(…)
    —— 触发重组。没有它,修改操作对运行时是不可见的。
对于集合类型,优先使用
mutableStateListOf
/
mutableStateMapOf
(同样需要配合
remember
)。它们会在每次读取和修改时触发Snapshot读写。如果使用
remember { mutableStateOf(mutableListOf<X>()) }
后调用
list.add(x)
,不会触发重组,因为
MutableList.add
不会经过State的setter——你必须替换整个值(
state = state + x
)。

When this rule does NOT apply

本规则不适用的场景

  • Inside
    remember { … }
    's producer block.
    That runs once per key change, not on every recompose. A local
    var
    there is fine:
    val builder = remember { mutableListOf<X>().apply { var n = 0; … } }
    .
  • In non-
    @Composable
    lambdas passed out of a composable.
    onClick = { var a = 0; … }
    is a plain
    () -> Unit
    . Local vars there are normal Kotlin.
  • In plain (non-
    @Composable
    ) helper functions.
    Only composable scopes are affected.
  • remember { … }
    的生产者块内部
    。该代码块仅在key变化时执行一次,而非每次重组。这里的本地var是安全的:
    val builder = remember { mutableListOf<X>().apply { var n = 0; … } }
  • 传递到Composable外部的非
    @Composable
    lambda
    onClick = { var a = 0; … }
    是普通的
    () -> Unit
    ,其中的本地var是正常的Kotlin变量。
  • 普通(非
    @Composable
    )辅助函数
    。仅Composable作用域受该规则影响。

2. The
@ReadOnlyComposable
contract

2.
@ReadOnlyComposable
契约

@ReadOnlyComposable
declares that a composable only reads composition state — no
Text
, no
Box
, no
remember
, no layout nodes, no positional slots. The runtime can then skip allocating a group for the call, which matters for fast accessor-style composables (
MaterialTheme.colorScheme
,
LocalDensity.current
, design-system token accessors).
The contract is bidirectional:
  • Add
    @ReadOnlyComposable
    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
    LocalFoo.current
    and returns a value).
  • 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.tertiary
kotlin
// ❌ 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()
@ReadOnlyComposable
声明该Composable仅读取组合状态——不调用
Text
Box
remember
,不创建布局节点或位置插槽。运行时可因此跳过为该调用分配分组,这对快速访问器式的Composable(如
MaterialTheme.colorScheme
LocalDensity.current
、设计系统令牌访问器)至关重要。
该契约是双向的
  • 添加
    @ReadOnlyComposable
    :当函数体调用的所有Composable本身都是
    @ReadOnlyComposable
    (或没有调用任何Composable——例如仅读取
    LocalFoo.current
    并返回值的函数)时。
  • 不要添加
    @ReadOnlyComposable
    :如果调用了任何非只读的Composable。该优化假设你不参与组合;违反契约会导致调用方出现不正确的重组行为。
kotlin
// ✅ 正确——仅读取组合本地变量,无布局或remember调用
@Composable
@ReadOnlyComposable
fun appSpacing(): Dp = LocalDimensions.current.spacing

// ✅ 正确——Composable属性getter;规则相同
val accent: Color
    @Composable @ReadOnlyComposable
    get() = MaterialTheme.colorScheme.tertiary
kotlin
// ❌ 错误——标记为只读但布局了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
    ,
    Text
    , anything from
    androidx.compose.foundation.layout
    or
    androidx.compose.material*
    .
  • A side-effect call:
    LaunchedEffect
    ,
    DisposableEffect
    ,
    SideEffect
    ,
    produceState
    .
  • remember { … }
    — positional memoization is composition state.
  • A
    @Composable
    lambda invocation (
    content()
    ).
  • An invocation of a non-
    @ReadOnlyComposable
    composable function.
If the body is only reading
Local*.current
, calling other
@ReadOnlyComposable
functions, or doing pure computation, add it.
如果函数体包含以下任意内容,请勿添加
@ReadOnlyComposable
  • 布局调用:
    Box
    Column
    Row
    LazyColumn
    Text
    ,以及
    androidx.compose.foundation.layout
    androidx.compose.material*
    中的任意函数。
  • 副作用调用:
    LaunchedEffect
    DisposableEffect
    SideEffect
    produceState
  • remember { … }
    —— 位置记忆属于组合状态。
  • @Composable
    lambda调用(
    content()
    )。
  • 调用非
    @ReadOnlyComposable
    的Composable函数。
如果函数体仅读取
Local*.current
、调用其他
@ReadOnlyComposable
函数或执行纯计算,请添加该注解。

When this rule does NOT apply

本规则不适用的场景

  • override fun
    declarations.
    The annotation is part of the contract; if the base isn't
    @ReadOnlyComposable
    , you can't make an override one. Refactor the base, or accept the override pays the group-creation cost.
  • Abstract declarations. No body to check.
  • override fun
    声明
    。注解是契约的一部分;如果基函数不是
    @ReadOnlyComposable
    ,你无法将重写函数标记为该注解。请重构基函数,或接受重写函数需要承担分组创建的开销。
  • 抽象声明。没有函数体可供检查。

Related: side effects live in their own skill

相关:副作用属于专属技能范畴

If a composable needs
LaunchedEffect
,
DisposableEffect
,
SideEffect
,
rememberCoroutineScope
,
rememberUpdatedState
,
snapshotFlow
, snackbar/navigation handling, analytics, or Flow collection, use
compose-side-effects
.
Focus splits by question: navigation, focus state,
FocusRequester
ownership, behavior
compose-focus-navigation
; when to call imperative
requestFocus
(effect timing, lifecycle, keys, API choice) →
compose-side-effects
.
This skill is about authoring Compose state correctly.
rememberUpdatedState
is effect capture state, not a general replacement for
remember { mutableStateOf(...) }
. Side effects have separate lifecycle and keying rules, and keeping them in one focused skill avoids two sources of truth.
如果Composable需要
LaunchedEffect
DisposableEffect
SideEffect
rememberCoroutineScope
rememberUpdatedState
snapshotFlow
、 snackbar/导航处理、分析或Flow收集,请使用
compose-side-effects
关注点按问题拆分:导航、焦点状态、
FocusRequester
所有权与行为
compose-focus-navigation
何时调用命令式
requestFocus
(副作用时机、生命周期、key、API选择)→
compose-side-effects
本技能专注于正确编写Compose状态。
rememberUpdatedState
是副作用捕获状态,并非
remember { mutableStateOf(...) }
的通用替代方案。副作用有独立的生命周期和key规则,将它们放在专属技能中可避免出现两套规则。

Quick reference

快速参考

SymptomDiagnosisFix
var x = …
inside
@Composable fun
body
Not recomposition-safe (§1)
var x by remember { mutableStateOf(…) }
var x = …
inside
Column { … }
/
Row { … }
content lambda
Same — content lambdas are
@Composable
(§1)
Same fix
remember { mutableStateOf(list) }
then
.add(x)
not recomposing
Mutation bypasses State setterUse
mutableStateListOf
, or replace the value:
state = state + x
@Composable fun
with no
Text
/
Box
/
remember
/effect calls
Could be
@ReadOnlyComposable
(§2)
Add
@ReadOnlyComposable
above
@Composable
@ReadOnlyComposable
function that calls
Box {}
/
Column {}
/ a normal composable
Contract violation (§2)Remove
@ReadOnlyComposable
症状诊断修复方案
@Composable fun
函数体内部出现
var x = …
不符合重组安全要求(§1)改为
var x by remember { mutableStateOf(…) }
Column { … }
/
Row { … }
内容lambda内部出现
var x = …
同上——内容lambda也是
@Composable
(§1)
相同修复方案
使用
remember { mutableStateOf(list) }
后调用
.add(x)
未触发重组
修改操作绕过了State的setter使用
mutableStateListOf
,或替换整个值:
state = state + x
@Composable fun
未调用
Text
/
Box
/
remember
/副作用函数
可标记为
@ReadOnlyComposable
(§2)
@Composable
上方添加
@ReadOnlyComposable
标记
@ReadOnlyComposable
的函数调用了
Box {}
/
Column {}
/ 普通Composable
违反契约(§2)移除
@ReadOnlyComposable

When NOT to apply

请勿应用的场景

  • Tests with
    composeTestRule.setContent { … }
    follow the same rules — they're production composables.
  • produceState
    has its own producer block that runs in a coroutine; you don't need
    LaunchedEffect
    inside it.
  • derivedStateOf
    has its own concerns around stability and equality — out of scope here; it's about preventing recomposition, not authoring state.
  • override
    s
    of read-only-composable declarations: the annotation is fixed by the base; you can't add or remove it locally.
  • 使用
    composeTestRule.setContent { … }
    测试代码遵循相同规则——它们属于生产级Composable。
  • **
    produceState
    **有自己的协程生产者块;无需在其内部使用
    LaunchedEffect
  • **
    derivedStateOf
    **涉及稳定性和相等性的相关问题——超出本技能范畴;它专注于避免重组,而非状态编写。
  • 重写只读Composable声明:注解由基函数决定;无法在本地添加或移除。

Red flags during review

代码审查中的危险信号

ThoughtReality
"It's a small composable, the bare
var
is fine"
Recomposition can fire at any time. The reset is non-deterministic by design — and a single bug report later.
"I'll add
@ReadOnlyComposable
because the function looks simple"
"Simple" isn't the criterion. "Makes only read-only calls" is.
"I always reach for
LaunchedEffect
because it's the one I know"
Use
compose-side-effects
; effect API choice depends on lifecycle and keys.
"I'll just
.add()
to the remembered list"
A
mutableStateOf(List)
doesn't observe internal mutation — use
mutableStateListOf
or replace the value.
"The override needs
@ReadOnlyComposable
to match what it does"
If the base isn't
@ReadOnlyComposable
, you can't add it to an override. Refactor the base instead.
错误想法实际情况
"这是个小型Composable,裸var没问题"重组可能随时触发。状态重置是设计上的非确定性问题——后续会出现Bug报告。
"这个函数看起来简单,我要加
@ReadOnlyComposable
"
"简单"不是判断标准。"仅调用只读函数"才是。
"我总是用
LaunchedEffect
,因为我只知道这个"
请使用
compose-side-effects
;副作用API的选择取决于生命周期和key。
"我直接在remember的列表上调用
.add()
就行"
mutableStateOf(List)
不会监听内部修改——请使用
mutableStateListOf
或替换整个值。
"重写函数需要
@ReadOnlyComposable
来匹配它的行为"
如果基函数不是
@ReadOnlyComposable
,你无法为重写函数添加该注解。请重构基函数。