24、副作用:LaunchedEffect、rememberCoroutineScope、snapshotFlow
Compose 的声明式 UI 看起来很美好——状态变了,界面自动更新。但真实项目里,总有一些「不听话」的操作:网络请求、数据库读写、定时器、动画回调……这些就是所谓的副作用。
说白了,副作用就是「在 Composable 函数之外干的事」。Compose 本身是纯 UI 描述,它不负责管理这些异步任务。那怎么办?官方给了我们三把利器:LaunchedEffect、rememberCoroutineScope 和 snapshotFlow。
我个人习惯把这三种工具按场景分开记:跟着生命周期走的用 LaunchedEffect,需要手动控制的用 rememberCoroutineScope,把 Compose 状态转成冷流的用 snapshotFlow。下面我们一个一个拆开讲。
LaunchedEffect:跟着 Composable 生死走
LaunchedEffect 会在 Composable 进入组合时启动一个协程,退出组合时自动取消。它的核心价值在于:你不用手动管理协程的生命周期。
来看一个最常见的场景——进入页面时加载数据:
@Composable
fun UserProfileScreen(userId: String) {
var userData by remember { mutableStateOf<User?>(null) }
LaunchedEffect(userId) {
// 这个协程会在 userId 变化时重启
userData = api.fetchUser(userId)
}
// UI 渲染...
}
这里有个关键点:LaunchedEffect 的第一个参数是 key。当 key 变化时,旧的协程会被取消,新的协程会启动。我在项目中遇到过一个问题——忘了传 key,结果页面切换了,协程还在跑,导致内存泄漏。嗯,这个坑踩过一次就记住了。
rememberCoroutineScope:手动控制协程
有时候你需要在 Composable 外部启动协程,比如按钮点击事件里。这时候 LaunchedEffect 就不太合适了,因为它只能在 Composable 内部用。
rememberCoroutineScope 会返回一个 CoroutineScope,这个 scope 的生命周期和当前 Composable 节点绑定。你可以在任何回调里用它启动协程:
@Composable
fun LoginButton() {
val scope = rememberCoroutineScope()
var isLoading by remember { mutableStateOf(false) }
Button(
onClick = {
scope.launch {
isLoading = true
try {
loginApi.login()
} finally {
isLoading = false
}
}
}
) {
Text(if (isLoading) "登录中..." else "登录")
}
}
你想想看,如果没有 rememberCoroutineScope,你就得自己搞一个 ViewModel 或者全局 scope,那生命周期管理就麻烦了。这个 API 说白了就是「给 Composable 里的回调一个安全的协程环境」。
snapshotFlow:把 Compose 状态转成冷流
这个 API 可能用得少一些,但非常强大。snapshotFlow 可以把 Compose 的 State 对象转成一个 Flow。每当状态变化时,Flow 就会发射新值。
我举个例子——监听搜索框的输入,做防抖搜索:
@Composable
fun SearchScreen() {
var query by remember { mutableStateOf("") }
// 把 query 转成 Flow
val debouncedQuery = snapshotFlow { query }
.debounce(300)
.flowOn(Dispatchers.Default)
LaunchedEffect(Unit) {
debouncedQuery.collect { value ->
// 在这里做网络搜索
searchApi.search(value)
}
}
TextField(
value = query,
onValueChange = { query = it }
)
}
这里 snapshotFlow { query } 会监听 query 这个 State 的变化。每次变化都会触发 Flow 的发射,然后我们利用 Flow 的 debounce 操作符做防抖。这个组合拳在搜索、过滤场景下特别好用。
三者的对比与选择
为了方便记忆,我画了一张对比图。你可以把它当作决策树来用:
这张图应该能帮你快速定位该用哪个。我再补充一个表格,把关键区别列出来:
| 特性 | LaunchedEffect | rememberCoroutineScope | snapshotFlow |
|---|---|---|---|
| 启动方式 | 自动,进入组合时 | 手动,在回调中调用 | 自动,状态变化时 |
| 生命周期 | 跟随 Composable | 跟随 Composable | 跟随收集它的协程 |
| 重启条件 | key 变化 | 手动控制 | State 变化 |
| 典型场景 | 页面初始化加载 | 按钮点击、手势回调 | 搜索防抖、状态监听 |
| 返回值 | 无(直接执行) | CoroutineScope | Flow<T> |
避坑指南
我曾经在项目里犯过一个低级错误——在 LaunchedEffect 里直接调用了 viewModel 的挂起函数,但忘了传 key。结果页面退出了,协程还在跑,导致 ViewModel 里的状态被更新,UI 却已经销毁了。嗯,后来我养成了一个习惯:所有和 UI 生命周期绑定的协程,一定要确认 key 是否正确。
还有一个坑:snapshotFlow 默认在 Dispatchers.Main 上收集。如果你在里面做耗时操作,记得用 .flowOn(Dispatchers.Default) 切换线程。不然 UI 会卡顿,用户可不会管你用的是不是 Compose。
最后说一句:副作用管理是 Compose 开发中绕不开的坎。但只要你掌握了 LaunchedEffect、rememberCoroutineScope 和 snapshotFlow 这三个工具,大部分场景都能轻松应对。我个人觉得,理解它们的生命周期差异比记住 API 签名更重要——API 可以查文档,但生命周期理解错了,bug 就藏得深了。
公众号:蓝海资料掘金营,微信deep3321