副作用 API:LaunchedEffect、DisposableEffect、SideEffect

源码仓库 :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. 自检清单

  1. 这段异步在 离开屏 时必须停吗?是 → LaunchedEffect / viewModelScope,不要用野 GlobalScope。
  2. LaunchedEffect 的 key 是否最小化?能否用 rememberUpdatedState 减少重启?
  3. 是否每个 DisposableEffect 都有对称的 onDispose?
  4. SideEffect 里是否有 重计算 / IO?有则迁走。

系列导航

《Modifier 链与顺序、测量与命中区域》

《状态 StateFlow、ViewModel 与 UI 收集》

相关推荐
Starry-sky(jing)2 小时前
Vidda ADB 开启:R Pro 65 找不到开发者选项的处理与连接验收
android·adb·远程控制·电视·vidda
雨白3 小时前
从原理到实践:使用 Kotlin KSP 仿写 ButterKnifeLite
android·kotlin
疯华整猫4 小时前
把二十七个 init 从首帧前面搬走:用 Trae 把启动优化钉进 CI
android·性能优化·ai编程
传奇开心果4 小时前
【探秘 Jetpack Compose 框架深水区】 第8课:rememberSaveable 的边界在哪里?——Bundle、Saver 与什么值得跨进程存活
android·kotlin
Frankie_stand6 小时前
测试自动化实战指南:从脚本化、自动判据到 CI 集成的完整落地路径
android·ci/cd·自动化
恋猫de小郭7 小时前
Flutter 大小核优化又被回滚,然后又开新提案?
android·前端·flutter
Kapaseker7 小时前
领导问我 Kotlin 的 Nothing 是什么?
android·kotlin
韩曙亮7 小时前
【错误记录】安卓 adb 调试 5037 端口占用排查 ( adb 的三层结构 | 找出本机占用 5037 的进程 )
android·数据库·adb·安卓·错误记录
三少爷的鞋8 小时前
不改变异步本质,只改变代码组织:Telegram 项目的 Kotlin 协程实践
android
好的佩奇17 小时前
蓝牙之广播与扫描
android·flutter·蓝牙