compose-side-effects
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCompose: side effects
Compose:副作用
Core principle
核心原则
Composable bodies describe UI. They can be recomposed, skipped, or abandoned. Work that changes the outside world belongs in an effect API whose lifecycle matches the work.
可组合函数的主体用于描述UI。它们可能会被重组、跳过或废弃。改变外部环境的操作应放在生命周期与操作匹配的effect API中。
Pick the smallest effect
选择最小粒度的Effect
| Need | API |
|---|---|
| Publish Compose state to non-Compose code after every successful recomposition | |
| Register/unregister a listener, callback, observer, or resource | |
| Run suspending, deferred, or keyed one-shot work | |
| Launch suspending work from a user event callback | |
| Convert Compose snapshot reads into a Flow inside a coroutine | |
| 需求 | API |
|---|---|
| 在每次成功重组后将Compose状态同步到非Compose代码 | |
| 注册/注销监听器、回调、观察者或资源 | |
| 执行挂起、延迟或基于键的一次性操作 | |
| 从用户事件回调中启动挂起操作 | |
| 在协程中将Compose快照读取转换为Flow | 在 |
Effect keys
Effect键
Keys define restart identity. When any key changes, the old effect is cancelled/disposed and a new one starts.
kotlin
// ✅ Restart collection when userId changes
LaunchedEffect(userId) {
repository.events(userId).collect { event -> handle(event) }
}
// ❌ Unit hides a changing input; collection keeps using the first userId
LaunchedEffect(Unit) {
repository.events(userId).collect { event -> handle(event) }
}Use stable, semantic keys:
- Use the thing whose lifecycle the effect follows: ,
userId,screenId,lifecycleOwner.focusRequester - Do not use broad objects (,
state) when only one property matters.viewModel - Do not add changing lambdas as keys unless you really want restarts on every lambda change.
键用于定义重启标识。当任意键发生变化时,旧的effect会被取消/销毁,新的effect会启动。
kotlin
// ✅ 当userId变化时重启收集操作
LaunchedEffect(userId) {
repository.events(userId).collect { event -> handle(event) }
}
// ❌ Unit隐藏了变化的输入;收集操作会一直使用第一个userId
LaunchedEffect(Unit) {
repository.events(userId).collect { event -> handle(event) }
}使用稳定且具有语义的键:
- 使用effect生命周期所跟随的对象:、
userId、screenId、lifecycleOwner。focusRequester - 当只有一个属性重要时,不要使用宽泛的对象(如、
state)。viewModel - 除非确实希望每次lambda变化时都重启,否则不要将变化的lambda作为键。
Avoid stale captures
避免过时捕获
For long-running effects that should not restart but need the latest callback or value, use .
rememberUpdatedStatekotlin
@Composable
fun Timeout(onTimeout: () -> Unit) {
val latestOnTimeout by rememberUpdatedState(onTimeout)
LaunchedEffect(Unit) {
delay(1_000)
latestOnTimeout()
}
}Use this when the lifecycle is "start once" but the invoked lambda should stay fresh. Common cases:
- A timeout or splash effect should not restart when changes, but it should call the latest callback.
onTimeout - A lifecycle observer should stay registered to the same owner, but invoke the latest /
onStartlambdas.onStop - A long-running collector should keep its collection lifecycle, but call the latest event handler.
Do not use to avoid choosing proper keys. If the changed value should restart the work, make it a key instead:
rememberUpdatedStatekotlin
// BAD: userId changes should restart the collection, not update a captured value.
val latestUserId by rememberUpdatedState(userId)
LaunchedEffect(Unit) {
repository.events(latestUserId).collect { event -> handle(event) }
}
// GOOD: the collection lifecycle follows userId.
LaunchedEffect(userId) {
repository.events(userId).collect { event -> handle(event) }
}rememberUpdatedStateStatecompose-state-deferred-reads对于不应重启但需要最新回调或值的长期运行effect,使用。
rememberUpdatedStatekotlin
@Composable
fun Timeout(onTimeout: () -> Unit) {
val latestOnTimeout by rememberUpdatedState(onTimeout)
LaunchedEffect(Unit) {
delay(1_000)
latestOnTimeout()
}
}在生命周期为“启动一次”但调用的lambda需要保持最新时使用此方法。常见场景:
- 超时或启动画面effect不应在变化时重启,但应调用最新的回调。
onTimeout - 生命周期观察者应保持注册到同一所有者,但调用最新的/
onStartlambdas。onStop - 长期运行的收集器应保持其收集生命周期,但调用最新的事件处理程序。
不要使用来避免选择合适的键。如果变化的值应该重启操作,应将其作为键:
rememberUpdatedStatekotlin
// 错误:userId变化应重启收集操作,而不是更新捕获的值。
val latestUserId by rememberUpdatedState(userId)
LaunchedEffect(Unit) {
repository.events(latestUserId).collect { event -> handle(event) }
}
// 正确:收集操作的生命周期跟随userId。
LaunchedEffect(userId) {
repository.events(userId).collect { event -> handle(event) }
}rememberUpdatedStateStatecompose-state-deferred-readsCollecting Flow
Flow收集
Use for side-effect/event flows: snackbars, navigation events, analytics events, focus commands, or other streams where each emission triggers imperative work.
LaunchedEffectkotlin
LaunchedEffect(events) {
events.collect { event ->
snackbarHostState.showSnackbar(event.message)
}
}Do not collect render state imperatively just to mutate local state. For UI state, collect near the state holder and pass plain values into the UI composable—the state-holder vs UI split, / , and preview-friendly wiring are covered in . Do not duplicate that architecture here.
collectAsStateWithLifecycle()collectAsState()compose-state-holder-ui-splitOn Android, prefer lifecycle-aware collection where available; use on targets without lifecycle-aware APIs.
collectAsState()For Compose state reads, use :
snapshotFlowkotlin
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.distinctUntilChanged()
.collect { index -> analytics.visibleIndex(index) }
}snapshotFlow { ... }.map { ... }collect对于副作用/事件流:snackbars、导航事件、分析事件、焦点命令或其他每次发射都会触发命令式操作的流,使用。
LaunchedEffectkotlin
LaunchedEffect(events) {
events.collect { event ->
snackbarHostState.showSnackbar(event.message)
}
}不要为了改变本地状态而命令式地收集渲染状态。对于UI状态,在状态持有者附近收集,并将普通值传入UI可组合函数——状态持有者与UI分离、 / 以及预览友好的连接方式在中有详细说明。请勿在此处重复该架构。
collectAsStateWithLifecycle()collectAsState()compose-state-holder-ui-split在Android上,优先使用支持生命周期感知的收集方式;在没有生命周期感知API的目标平台上使用。
collectAsState()对于Compose状态读取,使用:
snapshotFlowkotlin
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.distinctUntilChanged()
.collect { index -> analytics.visibleIndex(index) }
}没有终端的不会执行任何操作。
collectsnapshotFlow { ... }.map { ... }User events
用户事件
Use when a click or gesture starts suspending work:
rememberCoroutineScope()kotlin
@Composable
fun SaveButton(snackbarHostState: SnackbarHostState) {
val scope = rememberCoroutineScope()
Button(
onClick = {
scope.launch {
snackbarHostState.showSnackbar("Saved")
}
},
) {
Text("Save")
}
}Avoid "event flag" state just to trigger a . The click already is the event.
LaunchedEffect当点击或手势启动挂起操作时,使用:
rememberCoroutineScope()kotlin
@Composable
fun SaveButton(snackbarHostState: SnackbarHostState) {
val scope = rememberCoroutineScope()
Button(
onClick = {
scope.launch {
snackbarHostState.showSnackbar("Saved")
}
},
) {
Text("Save")
}
}避免仅为了触发而使用“事件标志”状态。点击本身就是事件。
LaunchedEffectRegistration and cleanup
注册与清理
Use for paired setup/teardown:
DisposableEffectkotlin
@Composable
fun ObserveLifecycle(owner: LifecycleOwner, observer: LifecycleObserver) {
DisposableEffect(owner, observer) {
owner.lifecycle.addObserver(observer)
onDispose {
owner.lifecycle.removeObserver(observer)
}
}
}Every registration path should have a matching cleanup path.
onDispose对于成对的设置/销毁操作,使用:
DisposableEffectkotlin
@Composable
fun ObserveLifecycle(owner: LifecycleOwner, observer: LifecycleObserver) {
DisposableEffect(owner, observer) {
owner.lifecycle.addObserver(observer)
onDispose {
owner.lifecycle.removeObserver(observer)
}
}
}每个注册路径都应有对应的清理路径。
onDisposeCommon mistakes
常见错误
| Mistake | Fix |
|---|---|
| Network request directly in the composable body | Usually move to a ViewModel/state holder; use |
| Analytics property written from the composable body | Use |
| Impression/event logged from the composable body | Use |
| Key by |
| Hidden lifecycle bug |
| Long-lived effect invokes an old callback after recomposition | Stale capture |
| Key by the specific property |
| Usually |
Listener added in | Use |
Launching from click by setting | Use |
| 错误 | 修复方案 |
|---|---|
| 在可组合函数主体中直接发起网络请求 | 通常移至ViewModel/状态持有者;仅将 |
| 在可组合函数主体中写入分析属性 | 当需要在每次成功重组后发布时,使用 |
| 在可组合函数主体中记录曝光/事件 | 当需要针对该键运行一次时,使用 |
| 以 |
使用 | 隐藏的生命周期问题 |
| 长期运行的effect在重组后调用旧的回调 | 过时捕获 |
| 以特定属性作为键 |
| 通常使用 |
在 | 使用 |
通过设置 | 在点击回调中使用 |
Red flags during review
代码审查中的危险信号
- "This only runs once" about code in a composable body.
- in a function with changing parameters.
LaunchedEffect(Unit) - A flow chain inside an effect with no terminal collection.
- Effects whose keys are chosen to silence lint instead of model lifecycle.
- Callback lambdas used from long-lived effects without either a key or .
rememberUpdatedState
- 关于可组合函数主体中代码的“这只运行一次”的描述。
- 在带有变化参数的函数中使用。
LaunchedEffect(Unit) - effect内的流链没有终端收集操作。
- 选择键是为了消除lint警告而非匹配生命周期的effect。
- 长期运行的effect使用回调lambda,但既未将其作为键也未使用。
rememberUpdatedState