协程与Compose:Compose中的协程使用、rememberCoroutineScope、LaunchedEffect与DisposableEffect、Compose中的Flow收集
Compose 和协程,说白了就是天生一对。你想想看,Compose 是声明式 UI,协程是异步编程的利器,两者结合能让你的代码既简洁又强大。我在项目中接手过一个遗留的页面,里面全是回调嵌套,改造成 Compose + 协程后,代码量直接砍了一半。嗯,今天我们就来聊聊这个组合拳怎么打。
Compose 中的协程环境
Compose 本身运行在 UI 线程上,但你不能在 Composable 函数里直接启动协程。为什么?因为 Composable 函数可能会被频繁重组,直接启动协程会导致泄漏或重复执行。你需要一个安全的协程作用域。
Compose 提供了三个核心工具来管理协程:
- rememberCoroutineScope:在 Composable 中获取一个协程作用域
- LaunchedEffect:在 Composable 进入组合时启动协程
- DisposableEffect:在 Composable 离开组合时清理资源
我个人习惯把 rememberCoroutineScope 用在用户交互触发的异步操作上,比如按钮点击。而 LaunchedEffect 更适合页面初始化时的数据加载。DisposableEffect 则用来注册和反注册监听器。
核心原则:不要在 Composable 函数体内直接调用 suspend 函数或启动协程。必须通过 rememberCoroutineScope、LaunchedEffect 或 DisposableEffect 来管理。
rememberCoroutineScope:手动控制协程生命周期
rememberCoroutineScope 返回一个 CoroutineScope,它的生命周期与当前 Composable 的“位置”绑定。当这个 Composable 从组合中移除时,作用域会自动取消,所有子协程也会被清理。
我最常用的场景是按钮点击事件。比如用户点击“刷新”按钮,我需要发起网络请求:
@Composable
fun RefreshButton() {
val scope = rememberCoroutineScope()
var isLoading by remember { mutableStateOf(false) }
Button(
onClick = {
scope.launch {
isLoading = true
try {
// 模拟网络请求
delay(2000)
// 更新 UI 状态
} finally {
isLoading = false
}
}
},
enabled = !isLoading
) {
Text(if (isLoading) "加载中..." else "刷新")
}
}
这里有个坑,我曾经踩过:如果你在 onClick 里直接使用 GlobalScope.launch,页面销毁后协程还在跑,轻则内存泄漏,重则更新已销毁的 UI 导致崩溃。rememberCoroutineScope 会自动处理这个问题。
小技巧:rememberCoroutineScope 默认使用 Dispatchers.Main,所以你在里面直接更新状态是安全的。如果需要切换线程,用 withContext(Dispatchers.IO) 即可。
LaunchedEffect:自动启动与取消
LaunchedEffect 会在 Composable 首次进入组合时启动一个协程,并在 Composable 离开组合或 key 发生变化时取消并重新启动。它的签名是:
@Composable
fun LaunchedEffect(
key1: Any?,
block: suspend CoroutineScope.() -> Unit
)
key 参数很关键。当 key 变化时,旧的协程会被取消,新的协程会启动。如果 key 不变,协程只会执行一次。我通常用它来做页面初始化加载:
@Composable
fun UserProfileScreen(userId: String) {
var userData by remember { mutableStateOf<UserData?>(null) }
LaunchedEffect(userId) {
// 当 userId 变化时,自动重新加载
userData = fetchUserData(userId)
}
// 渲染 UI
userData?.let {
Text("用户名: ${it.name}")
} ?: CircularProgressIndicator()
}
你想想看,如果用传统的回调方式,你得手动处理 userId 变化时的取消和重新请求。LaunchedEffect 帮你自动搞定了。我记得有一次重构一个列表详情页,用 LaunchedEffect 替换了原来的 LiveData + ViewModel 的加载逻辑,代码清晰了很多。
注意:LaunchedEffect 的 block 是挂起函数,如果你在里面启动子协程,记得用 coroutineScope 或 supervisorScope 来管理,否则子协程抛异常会导致整个 LaunchedEffect 取消。
DisposableEffect:资源清理的守护者
DisposableEffect 和 LaunchedEffect 类似,但它多了一个 onDispose 回调,专门用来清理资源。比如注册广播接收器、监听传感器、或者绑定生命周期观察者。
我曾经在项目中遇到一个问题:一个页面注册了 LocationManager 的位置监听,但页面退出时忘记取消注册,导致定位一直开着,耗电严重。用 DisposableEffect 就能完美解决:
@Composable
fun LocationTracker(
onLocationChanged: (Location) -> Unit
) {
val context = LocalContext.current
DisposableEffect(context) {
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
val listener = LocationListener { location ->
onLocationChanged(location)
}
// 注册监听
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
1000L,
0f,
listener
)
// 清理资源
onDispose {
locationManager.removeUpdates(listener)
}
}
}
onDispose 里的代码会在 Composable 离开组合时执行。不管是页面回退、配置变更还是被系统回收,它都会触发。嗯,这里要注意:onDispose 是同步执行的,不要在里面做耗时操作。
对比总结:
| API | 适用场景 | 生命周期 |
|---|---|---|
| rememberCoroutineScope | 用户交互触发的异步操作(点击、滑动) | 与 Composable 位置绑定 |
| LaunchedEffect | 页面初始化、数据加载、动画 | 进入组合时启动,离开或 key 变化时取消 |
| DisposableEffect | 注册/反注册监听器、绑定生命周期 | 进入组合时启动,离开时执行 onDispose |
Compose 中的 Flow 收集
Flow 是协程的产物,在 Compose 中收集 Flow 有几种方式。最直接的是用 collectAsState:
@Composable
fun TimerDisplay() {
val timeFlow = remember {
flow {
var seconds = 0
while (true) {
emit(seconds++)
delay(1000)
}
}
}
val seconds by timeFlow.collectAsState(initial = 0)
Text("已过去 $seconds 秒")
}
collectAsState 会把 Flow 转换为 State,每次 Flow 发射新值,Composable 就会重组。但这里有个性能问题:如果 Flow 发射频率很高(比如每秒几十次),重组次数也会很多。我建议在 ViewModel 层做防抖或采样。
另一种方式是结合 LaunchedEffect 手动收集:
@Composable
fun OrderStatusScreen(viewModel: OrderViewModel) {
var status by remember { mutableStateOf("等待中") }
LaunchedEffect(Unit) {
viewModel.statusFlow.collect { newStatus ->
status = newStatus
}
}
Text("订单状态: $status")
}
手动收集的好处是你可以控制收集的生命周期,比如在页面可见时才收集,不可见时暂停。我曾经用这种方式优化过一个实时行情页面,在页面不可见时停止收集,大大减少了不必要的网络请求。
最佳实践:
- 简单的 UI 状态用 collectAsState
- 需要精细控制生命周期时用 LaunchedEffect + collect
- Flow 发射频率高时,考虑使用 debounce、sample 等操作符
- 避免在 Composable 中创建 Flow,应该从 ViewModel 或 Repository 中获取
知识体系图
下面这张图展示了 Compose 中协程使用的核心脉络:
从图中可以看出,三个核心 API 各有侧重,但最终都指向 Flow 收集。在实际项目中,我通常这样组合使用:
- ViewModel 暴露 StateFlow 或 SharedFlow
- Composable 用 collectAsState 或 LaunchedEffect 收集
- 用户操作通过 rememberCoroutineScope 触发
- 资源监听用 DisposableEffect 管理
这套组合拳打下来,代码既安全又优雅。你想想看,以前用 LiveData + 生命周期观察者,写起来多啰嗦。现在一个 LaunchedEffect 就搞定了。嗯,这就是 Compose 的魅力所在。
避坑指南:
- 我曾经在 LaunchedEffect 里直接调用 viewModel 的 suspend 函数,结果因为 ViewModel 的 scope 和 Compose 的 scope 不一致,导致协程泄漏。后来统一用 viewModelScope 才解决。
- collectAsState 默认使用 Dispatchers.Main,如果你的 Flow 在 IO 线程发射,记得用 flowOn(Dispatchers.IO) 切换。
- DisposableEffect 的 onDispose 里不要调用 suspend 函数,它是同步回调。
好了,这一章的内容就到这里。记住这三个 API 的适用场景,你的 Compose 代码会变得既简洁又健壮。