Loading...
Loading...
Compare original and translation side by side
remember { … }remember { mutableStateOf(…) }mutableStateListOfmutableStateMapOf@ReadOnlyComposablerememberCoroutineScoperememberUpdatedStatecompose-side-effectsrememberLazyListStaterememberScrollStatecompose-state-deferred-readsFocusRequestercompose-focus-navigationremember { … }remember { mutableStateOf(…) }mutableStateListOfmutableStateMapOf@ReadOnlyComposablerememberCoroutineScoperememberUpdatedStatecompose-side-effectsrememberLazyListStaterememberScrollStatecompose-state-deferred-readsFocusRequestercompose-focus-navigation@Composablevarremember@ReadOnlyComposable@Composablevarremember@ReadOnlyComposablevar x = …@Composable funColumn { var x = … }@Composable fun@Composable get()@ReadOnlyComposableTextBoxColumnremember@Composable funColumn { var x = … }var x = …@Composable fun@Composable get()TextBoxColumnremember@ReadOnlyComposablevarvar// ❌ 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 + xvar// ❌ 错误——计数器在每次重组时重置;点击操作无法更新UI
@Composable
fun Counter() {
var count = 0
Button(onClick = { count++ }) { Text("$count") }
}
// ❌ 同样错误——该规则也适用于Composable内容lambda
@Composable
fun Wrapper() {
Row {
var count = 0 // Row的内容lambda也是@Composable
// …
}
}// ✅ 正确——`remember`确保值在重组后保留,`mutableStateOf`触发重组
@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@Composableremember { … }val builder = remember { mutableListOf<X>().apply { var n = 0; … } }@ComposableonClick = { var a = 0; … }() -> Unit@Composable@ReadOnlyComposable@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()@ReadOnlyComposableTextBoxrememberMaterialTheme.colorSchemeLocalDensity.current@ReadOnlyComposable@ReadOnlyComposableLocalFoo.current@ReadOnlyComposable// ✅ 正确——仅读取组合本地变量,无布局或remember调用
@Composable
@ReadOnlyComposable
fun appSpacing(): Dp = LocalDimensions.current.spacing
// ✅ 正确——Composable属性getter;规则相同
val accent: Color
@Composable @ReadOnlyComposable
get() = MaterialTheme.colorScheme.tertiary// ❌ 错误——标记为只读但布局了Box;违反契约
@Composable
@ReadOnlyComposable
fun Header(): Int {
Box {} // ← 非只读Composable调用
return 42
}
// ❌ 错误——从只读Composable调用普通Composable
@Composable
@ReadOnlyComposable
fun computed(): Int = nonReadOnlyHelper()@ReadOnlyComposableBoxColumnRowLazyColumnTextandroidx.compose.foundation.layoutandroidx.compose.material*LaunchedEffectDisposableEffectSideEffectproduceStateremember { … }@Composablecontent()@ReadOnlyComposableLocal*.current@ReadOnlyComposable@ReadOnlyComposableBoxColumnRowLazyColumnTextandroidx.compose.foundation.layoutandroidx.compose.material*LaunchedEffectDisposableEffectSideEffectproduceStateremember { … }@Composablecontent()@ReadOnlyComposableLocal*.current@ReadOnlyComposableoverride fun@ReadOnlyComposableoverride fun@ReadOnlyComposableLaunchedEffectDisposableEffectSideEffectrememberCoroutineScoperememberUpdatedStatesnapshotFlowcompose-side-effectsFocusRequestercompose-focus-navigationrequestFocuscompose-side-effectsrememberUpdatedStateremember { mutableStateOf(...) }LaunchedEffectDisposableEffectSideEffectrememberCoroutineScoperememberUpdatedStatesnapshotFlowcompose-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 |
| 症状 | 诊断 | 修复方案 |
|---|---|---|
| 不符合重组安全要求(§1) | 改为 |
| 同上——内容lambda也是 | 相同修复方案 |
使用 | 修改操作绕过了State的setter | 使用 |
| 可标记为 | 在 |
标记 | 违反契约(§2) | 移除 |
composeTestRule.setContent { … }produceStateLaunchedEffectderivedStateOfoverridecomposeTestRule.setContent { … }produceStateLaunchedEffectderivedStateOf| 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的列表上调用 | |
"重写函数需要 | 如果基函数不是 |