很多人接触 Compose 时,最先用上的往往是状态和布局,真正到了面试或项目排查阶段,才会发现副作用 API 才是最容易答不清的一块。要把这类问题说透,关键不是背概念,而是抓住三个判断点:副作用在什么时候触发、重组时会不会重跑、资源或协程在离开界面时如何清理。
什么是 Compose 里的副作用
在 Composable 执行过程中,只要代码会对外部世界产生影响,都可以算作副作用。常见例子包括弹 Toast、发起网络请求、注册监听、写日志、启动协程等。
问题在于,Composable 会因为状态变化不断重组。如果把这类操作直接写在普通组合代码里,它们就可能随着重组被反复执行,结果往往不是我们想要的。Compose 提供的副作用 API,本质上就是把这些“会影响外部”的操作,绑定到 Composable 生命周期中的特定阶段去执行。
理解这件事后,后面几个 API 的区别就容易把握了:有的适合做注册和释放,有的只在重组成功后触发,有的专门负责启动可取消的协程任务。
`DisposableEffect` 与 `SideEffect`:一个管生命周期,一个管重组结果
`DisposableEffect` 适合做进入时初始化、离开时释放
在传统 View 或 Activity 开发里,很多逻辑会放在 onCreate 和 onDestroy 里:例如注册监听、初始化资源、释放回调。到了 Compose 里,如果一个 Composable 也需要在进入活跃状态时做初始化、在离开时做清理,最常用的就是 DisposableEffect。

DisposableEffect 的参数由若干个 key 和最后的 block 组成。key 决定这个副作用什么时候重新执行。比如把 key 写成 Unit,意味着只在 Composable 进入活跃阶段时执行一次;换成 true 或 false,效果本质上也一样,因为它们都不会变化。
这个 API 有一个很重要的约束:在 block 内必须显式写出 onDispose。如果不写,编辑器会直接报错。原因也很明确,DisposableEffect 的核心价值就是“建立”和“释放”成对出现,Compose 要求你明确告诉它清理动作是什么。
DisposableEffect(Unit) {
// 初始化或注册逻辑
onDispose {
// 释放资源或取消注册
}
}
如果把 key 换成一个可变参数,例如 count,那么执行时机就不再只有首次进入。此时除了初次激活之外,每当 count 变化,原来的副作用会先触发一次 onDispose,然后再执行新的 block。也就是说,它同时具备“感知进入”和“感知关键参数变化”的能力。
所以可以把它理解为:DisposableEffect 适合那些必须严格成对管理的副作用,尤其是注册/反注册、开启/关闭、绑定/解绑这一类操作。
`SideEffect` 只会在一次重组成功提交后执行
SideEffect 也与重组有关,但它关注的不是“进入和离开”,而是“这次重组到底有没有成功完成”。它的执行时机不是开始重组时,而是一次重组成功结束之后。

这点非常关键。很多人会误以为只要发生重组,SideEffect 就一定会执行。实际上并不是。只有当本次重组顺利完成、结果成功提交后,SideEffect 里的代码才会运行。
原文提到过一个实验:在 Composable 中创建 100 个 Text,并分别在重组开始位置和 SideEffect 中打印时间。日志能看出,开始重组和执行 SideEffect 之间存在明显间隔,这个间隔本质上就是重组完成所花的时间。
反过来说,如果重组中途抛出异常,导致本次重组没有成功完成,那么 SideEffect 根本不会执行。这也是它和其他副作用 API 很容易混淆、但实际语义完全不同的地方。
如果需要把 Compose 内部状态同步到某个外部对象,并且要求“只有界面成功更新后才同步”,那 SideEffect 就比直接写在组合代码里更合适。
`LaunchedEffect` 与 `rememberCoroutineScope`:什么时候让协程跟着界面走
`LaunchedEffect` 适合由 key 驱动的异步任务
LaunchedEffect 和 DisposableEffect 在形式上很像,也会接收若干个观察参数 key 和一个 block,但它们最大的区别在于:LaunchedEffect 的 block 是挂起函数,会在协程中执行。
这意味着它天然适合做异步工作,例如网络请求、延时任务、Flow 收集等。你可以把它理解成“与 Composable 生命周期绑定的协程启动器”。
LaunchedEffect(id) {
// 发起网络请求、收集 Flow、执行延时任务等
}
它的执行规则可以拆成三点:
- 当 key 第一次进入组合时,协程启动一次;
- 只要 key 不变,即使 Composable 反复重组,协程也不会重启;
- 一旦 key 变化,旧协程会自动取消,新协程会重新启动。
此外,当 Composable 离开组合进入销毁阶段时,LaunchedEffect 内部协程也会自动取消。这就是为什么它不需要像 DisposableEffect 那样手写 onDispose:协程生命周期已经由 Compose 接管了。
如果面试里被问到它和 DisposableEffect 的区别,一个够用的回答就是:前者主要管理协程型副作用,取消和重启由 key 与生命周期自动处理;后者主要管理需要显式清理的副作用,必须提供 onDispose。
`rememberCoroutineScope` 适合在事件或其他作用域里启动协程
有些场景下,当前 Composable 已经在使用 DisposableEffect,但副作用内部又需要启动协程,同时还希望在 Composable 离开时自动取消。这时不一定非要把整个逻辑改写成 LaunchedEffect,还可以使用 rememberCoroutineScope。
rememberCoroutineScope 会创建一个 CoroutineScope,这个作用域同样绑定在当前 Composable 上。通过它启动的协程,会在 Composable 进入 onDispose 阶段时自动取消。
val scope = rememberCoroutineScope()
它尤其适合两类场景:
- 在按钮
onClick、手势回调等非 Composable 上下文中启动协程; - 在已有的
DisposableEffect逻辑中补充协程任务,而不想整体改成LaunchedEffect。
原文也提到,如果 DisposableEffect 观察的参数本身是可变的,那么随着参数变化,关联协程也会被取消并重新开启。实际开发里,是否采用这种方式,取决于你想让“副作用的重建”由谁控制:是由 DisposableEffect 的 key 驱动,还是直接由 LaunchedEffect 的 key 驱动。
副作用里如何拿到最新状态:`rememberUpdateState` 与 `snapshotFlow`
`rememberUpdateState` 解决“协程不重启,但值要更新”
有时我们希望副作用内部的协程长期存在,不要因为重组而反复重启。最直接的做法是让 LaunchedEffect 使用稳定、不变化的 key。但这样又会带来另一个问题:协程内部读取到的外部值,可能停留在旧版本。
这种情况下,rememberUpdateState 就派上用场了。它的作用不是触发副作用重启,而是让副作用内部在不重启的前提下,始终能读到外界的最新值。
原文中的例子是:在一个副作用中结合 Flow 定时打印状态值,协程本身不会随着重组重启;但由于打印的值来自 rememberUpdateState,当外界通过按钮点击让该值不断加一时,副作用里打印出来的内容也会同步变化。
换句话说,rememberUpdateState 解决的是“任务继续跑,但读取内容要更新”的问题。它很适合定时器、动画监听、回调桥接这类不希望频繁取消重建的长生命周期任务。
`snapshotFlow` 适合在副作用中持续监听状态变化
如果需求更进一步,不只是想在副作用里读到最新值,而是想把 Compose 状态变化转成流式事件持续收集,那么更适合的 API 是 snapshotFlow。
snapshotFlow 会订阅所读取状态的变化,一旦数据更新,就把新值发送到下游。它常常搭配 LaunchedEffect 使用,用来在协程里持续监听某个 Compose 状态。
LaunchedEffect(Unit) {
snapshotFlow { state }
.collect {
// 处理 state 的变化
}
}
原文中的例子里,按钮每点击一次都会改变 state,而 LaunchedEffect 内部通过 snapshotFlow 监听这个值的变化。根据日志,点击到数据被收集到之间,通常只有几毫秒到十几毫秒的延迟,足以应对大多数界面联动需求。
它和 rememberUpdateState 的侧重点不同:
rememberUpdateState更像是“让已有副作用读取到最新值”;snapshotFlow更像是“把状态变化主动推送进副作用处理链”。
如果你只是不想因为参数变化而重启协程,但需要在协程内部取最新值,用 rememberUpdateState 更轻;如果你需要持续观察变化、做流式处理、配合 collect 执行联动逻辑,那么 snapshotFlow 更合适。
怎么选:用一句话区分这些副作用 API
把前面的内容压缩一下,可以得到一套更适合面试和实战的判断方法:
DisposableEffect:需要显式注册和释放的副作用,关注进入与离开,必须写onDispose。SideEffect:只想在一次重组成功完成后执行代码,重组中或重组失败都不会触发。LaunchedEffect:需要与 Composable 生命周期绑定的异步任务,key 变化会取消旧协程并重启新协程。rememberCoroutineScope:需要在事件回调、DisposableEffect等位置手动启动协程,但仍希望协程跟随当前 Composable 生命周期。rememberUpdateState:不想重启副作用,只想让副作用内部读到外界最新值。snapshotFlow:想在副作用中持续监听 Compose 状态变化,并把它转成 Flow 处理。
如果非要再进一步总结,最实用的区分方式其实只有两步:先判断这段逻辑是不是异步协程任务,再判断它是要“读取最新值”还是“订阅值变化”。把这两个问题想清楚,大多数 Compose 副作用场景都不难归类。







