compose-side-effects

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Compose: 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

NeedAPI
Publish Compose state to non-Compose code after every successful recomposition
SideEffect
Register/unregister a listener, callback, observer, or resource
DisposableEffect(keys...)
Run suspending, deferred, or keyed one-shot work
LaunchedEffect(keys...)
Launch suspending work from a user event callback
rememberCoroutineScope()
Convert Compose snapshot reads into a Flow inside a coroutine
snapshotFlow { ... }
inside
LaunchedEffect
需求API
在每次成功重组后将Compose状态同步到非Compose代码
SideEffect
注册/注销监听器、回调、观察者或资源
DisposableEffect(keys...)
执行挂起、延迟或基于键的一次性操作
LaunchedEffect(keys...)
从用户事件回调中启动挂起操作
rememberCoroutineScope()
在协程中将Compose快照读取转换为Flow
LaunchedEffect
内使用
snapshotFlow { ... }

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
    ,
    viewModel
    ) when only one property matters.
  • 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
rememberUpdatedState
.
kotlin
@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
    onTimeout
    changes, but it should call the latest callback.
  • A lifecycle observer should stay registered to the same owner, but invoke the latest
    onStart
    /
    onStop
    lambdas.
  • A long-running collector should keep its collection lifecycle, but call the latest event handler.
Do not use
rememberUpdatedState
to avoid choosing proper keys. If the changed value should restart the work, make it a key instead:
kotlin
// 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) }
}
rememberUpdatedState
also does not make render state "non-recomposing." If the UI needs to display a changing value, read normal
State
in composition or use the deferred-read patterns in
compose-state-deferred-reads
for frame-rate values.
对于不应重启但需要最新回调或值的长期运行effect,使用
rememberUpdatedState
kotlin
@Composable
fun Timeout(onTimeout: () -> Unit) {
    val latestOnTimeout by rememberUpdatedState(onTimeout)

    LaunchedEffect(Unit) {
        delay(1_000)
        latestOnTimeout()
    }
}
在生命周期为“启动一次”但调用的lambda需要保持最新时使用此方法。常见场景:
  • 超时或启动画面effect不应在
    onTimeout
    变化时重启,但应调用最新的回调。
  • 生命周期观察者应保持注册到同一所有者,但调用最新的
    onStart
    /
    onStop
    lambdas。
  • 长期运行的收集器应保持其收集生命周期,但调用最新的事件处理程序。
不要使用
rememberUpdatedState
来避免选择合适的键。如果变化的值应该重启操作,应将其作为键:
kotlin
// 错误: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) }
}
rememberUpdatedState
也不会使渲染状态“不参与重组”。如果UI需要显示变化的值,在组合中读取普通
State
,或者对于帧率相关的值,使用
compose-state-deferred-reads
中的延迟读取模式。

Collecting Flow

Flow收集

Use
LaunchedEffect
for side-effect/event flows: snackbars, navigation events, analytics events, focus commands, or other streams where each emission triggers imperative work.
kotlin
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,
collectAsStateWithLifecycle()
/
collectAsState()
, and preview-friendly wiring are covered in
compose-state-holder-ui-split
. Do not duplicate that architecture here.
On Android, prefer lifecycle-aware collection where available; use
collectAsState()
on targets without lifecycle-aware APIs.
For Compose state reads, use
snapshotFlow
:
kotlin
LaunchedEffect(listState) {
    snapshotFlow { listState.firstVisibleItemIndex }
        .distinctUntilChanged()
        .collect { index -> analytics.visibleIndex(index) }
}
snapshotFlow { ... }.map { ... }
without a terminal
collect
does nothing.
对于副作用/事件流:snackbars、导航事件、分析事件、焦点命令或其他每次发射都会触发命令式操作的流,使用
LaunchedEffect
kotlin
LaunchedEffect(events) {
    events.collect { event ->
        snackbarHostState.showSnackbar(event.message)
    }
}
不要为了改变本地状态而命令式地收集渲染状态。对于UI状态,在状态持有者附近收集,并将普通值传入UI可组合函数——状态持有者与UI分离
collectAsStateWithLifecycle()
/
collectAsState()
以及预览友好的连接方式在
compose-state-holder-ui-split
中有详细说明。请勿在此处重复该架构。
在Android上,优先使用支持生命周期感知的收集方式;在没有生命周期感知API的目标平台上使用
collectAsState()
对于Compose状态读取,使用
snapshotFlow
kotlin
LaunchedEffect(listState) {
    snapshotFlow { listState.firstVisibleItemIndex }
        .distinctUntilChanged()
        .collect { index -> analytics.visibleIndex(index) }
}
没有终端
collect
snapshotFlow { ... }.map { ... }
不会执行任何操作。

User events

用户事件

Use
rememberCoroutineScope()
when a click or gesture starts suspending work:
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
LaunchedEffect
. The click already is the event.
当点击或手势启动挂起操作时,使用
rememberCoroutineScope()
kotlin
@Composable
fun SaveButton(snackbarHostState: SnackbarHostState) {
    val scope = rememberCoroutineScope()

    Button(
        onClick = {
            scope.launch {
                snackbarHostState.showSnackbar("Saved")
            }
        },
    ) {
        Text("Save")
    }
}
避免仅为了触发
LaunchedEffect
而使用“事件标志”状态。点击本身就是事件。

Registration and cleanup

注册与清理

Use
DisposableEffect
for paired setup/teardown:
kotlin
@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
onDispose
cleanup path.
对于成对的设置/销毁操作,使用
DisposableEffect
kotlin
@Composable
fun ObserveLifecycle(owner: LifecycleOwner, observer: LifecycleObserver) {
    DisposableEffect(owner, observer) {
        owner.lifecycle.addObserver(observer)
        onDispose {
            owner.lifecycle.removeObserver(observer)
        }
    }
}
每个注册路径都应有对应的
onDispose
清理路径。

Common mistakes

常见错误

MistakeFix
Network request directly in the composable bodyUsually move to a ViewModel/state holder; use
LaunchedEffect
only for UI-owned keyed work
Analytics property written from the composable bodyUse
SideEffect
when it should publish after every successful recomposition
Impression/event logged from the composable bodyUse
LaunchedEffect(key)
when it should run once for that key
LaunchedEffect(Unit)
captures changing
id
Key by
id
, or use
rememberUpdatedState
if it must not restart
rememberUpdatedState(id)
used so
LaunchedEffect(Unit)
keeps running after
id
changes
Hidden lifecycle bug
Long-lived effect invokes an old callback after recompositionStale capture
LaunchedEffect(state) { ... }
restarts too often
Key by the specific property
LaunchedEffect(...) { nonSuspendSetter() }
Usually
SideEffect
; keep
LaunchedEffect
only for keyed one-shot/deferred work
Listener added in
LaunchedEffect
with no cleanup
Use
DisposableEffect
Launching from click by setting
shouldShowSnackbar = true
Use
rememberCoroutineScope()
in the click callback
错误修复方案
在可组合函数主体中直接发起网络请求通常移至ViewModel/状态持有者;仅将
LaunchedEffect
用于UI所属的基于键的操作
在可组合函数主体中写入分析属性当需要在每次成功重组后发布时,使用
SideEffect
在可组合函数主体中记录曝光/事件当需要针对该键运行一次时,使用
LaunchedEffect(key)
LaunchedEffect(Unit)
捕获了变化的
id
id
作为键,或者如果必须不重启则使用
rememberUpdatedState
使用
rememberUpdatedState(id)
使
LaunchedEffect(Unit)
id
变化后继续运行
隐藏的生命周期问题
长期运行的effect在重组后调用旧的回调过时捕获
LaunchedEffect(state) { ... }
过于频繁地重启
以特定属性作为键
LaunchedEffect(...) { nonSuspendSetter() }
通常使用
SideEffect
;仅将
LaunchedEffect
用于基于键的一次性/延迟操作
LaunchedEffect
中添加监听器但未清理
使用
DisposableEffect
通过设置
shouldShowSnackbar = true
从点击事件启动操作
在点击回调中使用
rememberCoroutineScope()

Red flags during review

代码审查中的危险信号

  • "This only runs once" about code in a composable body.
  • LaunchedEffect(Unit)
    in a function with changing parameters.
  • 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