前言
在前面的文章中,我们反复强调:Composable 函数应该是纯净的(Pure)。 它的职责只是接收状态并渲染 UI。
但在真实的世界里,我们需要发起网络请求、需要弹一个 Toast、需要监听传感器的变化、或者需要初始化一个视频播放器。这些操作超出了"渲染 UI"的范畴,我们称之为 "副作用(Side Effects)"。
作为资深开发,我们习惯于在 onCreate、onResume、onDestroy 里处理这些逻辑。但在 Compose 这种"随心所欲"重组的环境下,如果处理不当,你的 App 可能会出现无限请求、内存泄漏甚至离奇崩溃。
今天,我们就来系统掌握 Compose 提供的副作用 API 家族。
一、 为什么 Composable 函数体里不能直接写业务逻辑?
看这个典型的错误示范:
kotlin
@Composable
fun UserProfile(userId: String) {
val user = loadUserFromApi(userId) // ❌ 致命错误!
Text("用户名:${user.name}")
}
原因: 重组可能由于任何原因(比如一个动画、一次点击)每秒发生 60 次。如果直接在函数体里写,意味着你的 API 请求也会每秒发 60 次。
我们需要一套机制,让逻辑**"只在必要的时候、且以受控的方式"**运行。
二、 核心副作用 API:对症下药
1. LaunchedEffect:协程的避风港
当你需要在进入页面时启动一个协程(如加载数据、倒计时)时,它是首选。
- 特性:绑定在 Composable 的生命周期上。进入组合时启动,离开时自动取消。
- Key 机制 :它接收一个
key。只有当key变化时,它才会重启。
kotlin
LaunchedEffect(userId) { // 只有 userId 变了,才会重新请求
viewModel.loadData(userId)
}
2. rememberCoroutineScope:事件驱动的协程
如果你想在点击按钮时启动协程(比如显示一个 SnackBar),你不能用 LaunchedEffect(因为它必须在 Composable 内部声明)。
- 用法 :获取一个与当前 Composable 绑定的
Scope,然后手动启动。
kotlin
val scope = rememberCoroutineScope()
Button(onClick = {
scope.launch { /* 异步操作 */ }
}) { ... }
3. DisposableEffect:有始有终的清理
需要注册监听器、初始化第三方 SDK,并在 UI 销毁时反注册?选它。
- 强制要求 :必须以
onDispose { ... }结尾。
kotlin
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { ... }
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer) // 保证不内存泄漏
}
}
4. SideEffect:与非 Compose 代码同步
如果你需要将 Compose 内部的状态同步给外部(比如一个由 C++ 维护的渲染器),可以使用它。它在每次成功重组后执行。
三、 高级技巧:处理"长跑"中的变化
rememberUpdatedState:防范闭门羹
想象一个场景:你在 LaunchedEffect 里开了一个 10 秒的定时器,在这 10 秒内,外部传进来的参数变了。默认情况下,协程由于没结束,它拿到的还是 10 秒前的旧值。
- 方案 :使用
rememberUpdatedState。它能保证协程内部始终引用最新的值,而不需要重启协程。
四、 给开发者的架构建议
- 副作用尽量往上提,甚至提进 ViewModel : 在 MVI 或 MVVM 架构中,复杂的业务逻辑(网络、DB)应该在 ViewModel 中处理。Compose 的副作用 API 更多是处理 "UI 相关" 的系统交互(如监听返回键、处理生命周期回调)。
- 谨慎选择 Key :
LaunchedEffect(true)意味着只在第一次进入组合时运行。这在处理"进入页面即统计"时很有用。 - 永远不要忘记
onDispose: 作为一个资深开发,对内存泄漏的警惕应该是本能。任何使用了DisposableEffect的地方,都要反复确认清理逻辑是否闭环。
结语
掌握了副作用 API,你就真正拿到了 Compose 的"控制权"。你不再是被动等待重组的看客,而是能够精确调度逻辑的导演。
下一篇我们将探讨:CompositionLocal:隐式数据传递的利弊与最佳实践。如果你觉得有帮助,欢迎点赞关注,我们在代码上演进,在原理上深耕。