源码仓库 :ComposeDemo(分支
main)
技术目标
分清三个 API 在 组合(Composition)生命周期 里的位置:何时启动、何时取消、能否挂起、与 重组 的关系。
| API | 调度 | 能否挂起 | 典型用途 |
|---|---|---|---|
LaunchedEffect(keys...) |
协程(Main 等由调用上下文决定) |
能 | 读 Flow、一次性异步、与 key 绑定的轮询/动画驱动 |
DisposableEffect(keys...) |
组合阶段,非挂起 | 否 | 注册/反注册:监听、回调、onDispose 释放 |
SideEffect |
每次组合 成功提交到树之后 | 否 | 把最新 Compose 状态同步到非 Compose 世界(极窄场景) |
1. 生命周期直觉(与重组对比)
离开或 key 变化
进入 Composition
组合阶段: 计算 UI 结构
SideEffect 回调
LaunchedEffect 启动协程
DisposableEffect: effect 块运行
DisposableEffect.onDispose
取消 LaunchedEffect 协程
- 重组 :同一
LaunchedEffect(key)若 key 全相等 ,内部协程 不重启;这是与「每次重组都跑」的直觉最大的差别。 SideEffect:在组合应用到树上之后执行;每次成功组合都可能执行(除非被跳过),不要把重活放这里。
2. LaunchedEffect 语义(必须背下来)
kotlin
LaunchedEffect(key1, key2) {
// block 在协程中执行;key 全相等时不会重启
// key 任一变化 → 旧 Job cancel → 新 Job 启动
}
- 不要 用
LaunchedEffect(Unit)当「应用级只初始化一次」:离开 composition 会 取消 ;且导航 离开再进入 会 再跑 ------若你要的是进程级单次初始化,应放在Application/ 显式单例初始化,而不是 Composable。 - 要 在
block里使用 可取消 的挂起:awaitCancellation()、ensureActive()、对支持取消的 IO 用正确 API;while(true)必须配合取消与delay/yield。 - 与
rememberCoroutineScope().launch对比 :LaunchedEffect的 Job 与 组合生命周期 自动绑定;rememberCoroutineScope适合 用户点击触发 、且需在回调里launch的场景,但要自己处理 重复点击、结构化并发(见 01 篇 ViewModel 边界)。
3. DisposableEffect 与资源释放
kotlin
DisposableEffect(Unit) {
val listener = ...; register(listener)
onDispose { unregister(listener) }
}
onDispose一定 在离开 composition 或 key 变化导致 effect 重建之前 调用。- 忘记
onDispose→ 泄漏 / 重复回调 / 双注册。 - key 选错 :例如把高频
state放进 key → 每次变化都 dispose/register,性能与正确性都会炸;应拆成「需要随某 id 重绑」的 key 与「用rememberUpdatedState读最新值」的组合。
4. SideEffect vs LaunchedEffect
SideEffect { ... }不能挂起;适合「把当前帧已确定的值写给外部」这类同步、极轻的逻辑。- 本仓库
StabilityLabScreen.kt用SideEffect统计子项组合次数是 演示用技巧 ,生产勿直接当性能计数器;正式性能分析用 Layout Inspector / Composition tracing 等工具。
5. 仓库示例:SideEffectSampleScreen

SideEffectSampleScreen.kt 行为对照:
| 机制 | 本屏实现 | 你应观察到的现象 |
|---|---|---|
LaunchedEffect(launchKey) |
launchKey 为 rememberSaveable 的 Int;点按钮 ++ |
key 变 → 日志追加一行带时间戳的 LaunchedEffect 记录;旧协程被取消 |
DisposableEffect(Unit) |
Unit 固定 → 进入屏 注册 一次;离开屏 onDispose |
日志出现 register;返回上一屏 时出现 onDispose |
| 日志 UI | mutableStateListOf + reversed() 展示 |
验证 列表重组 与副作用日志的联动 |
6. 与 ViewModel 的边界(技术决策)
| 副作用形态 | 更常见归属 | 原因 |
|---|---|---|
| 与界面 同生共死 的短请求、收集 UI 层 Flow | LaunchedEffect |
随屏取消,避免泄漏 |
| 与 配置/进程 无关、需跨旋转保留的业务任务 | ViewModel.viewModelScope |
与 ViewModelStore 对齐 |
| 系统广播、SDK 回调、需要 明确 register 对 | DisposableEffect |
onDispose 与组合对齐 |
7. 风险清单
LaunchedEffect内while(true)不检查取消 → 协程无法退出/work 泄漏。- 多个
LaunchedEffect隐式依赖 执行顺序 → 难测、易竞态;能合并的合并,或上移到 ViewModel 串行化。 rememberCoroutineScope().launch在 dispose 后仍持有 UI 引用 → 泄漏;长任务仍优先 ViewModel。- 把 导航、Snackbar 等一次性事件全塞进
LaunchedEffect轮询State:不如 01 篇的 Effect 通道 清晰。
8. 自检清单
- 这段异步在 离开屏 时必须停吗?是 →
LaunchedEffect/viewModelScope,不要用野GlobalScope。 LaunchedEffect的 key 是否最小化?能否用rememberUpdatedState减少重启?- 是否每个
DisposableEffect都有对称的onDispose? SideEffect里是否有 重计算 / IO?有则迁走。