第1章:Compose中的生命周期与EffectHandler
各位同学,今天我们来聊聊Compose里一个特别重要的概念——生命周期。嗯,准确地说,是EffectHandler那一整套东西。
我记得刚开始从View体系转到Compose时,最不适应的就是生命周期管理。在传统Android里,Activity有onCreate、onResume、onPause,你很清楚什么时候该干什么。但Compose不一样,它是个声明式框架,组件随时可能重组,你没法用老思路去管理副作用。
说白了,EffectHandler就是Compose给咱们提供的「生命周期钩子」。它让你在函数式UI里,依然能优雅地处理定时器、网络请求、资源释放这些事儿。
核心要点:Compose的生命周期不是组件本身的生老病死,而是重组过程中的副作用管理。你想想看,一个Composable函数可能被调用成百上千次,如果每次调用都开个网络请求,那不就炸了?
1.1 LaunchedEffect:启动协程的正确姿势
LaunchedEffect是我用得最多的一个。它的作用很简单:在Composable进入组合时启动一个协程,离开组合时自动取消。
我在项目中遇到过这样一个场景:用户进入详情页,需要加载商品数据。如果用传统方式,你得在onResume里启动协程,在onPause里取消。但在Compose里,一行LaunchedEffect就搞定了。
@Composable
fun ProductDetailScreen(productId: String) {
var product by remember { mutableStateOf<Product?>(null) }
LaunchedEffect(productId) {
// 这里的协程会在productId变化时重启
product = api.fetchProduct(productId)
}
// UI渲染...
}
注意看,LaunchedEffect的第一个参数是key。当key变化时,旧的协程会被取消,新的协程会启动。这个机制特别适合「依赖某个参数变化而重新执行」的场景。
我的习惯:如果LaunchedEffect只需要执行一次(比如页面初始化),key传Unit或true就行。千万别传一个会频繁变化的值,否则协程反复重启,性能就崩了。
1.2 DisposableEffect:别忘了清理资源
DisposableEffect和LaunchedEffect很像,但它多了一个「清理」回调。说白了,就是当Composable离开组合时,给你一个机会做资源释放。
我曾经踩过一个坑:在Compose里注册了一个BroadcastReceiver,结果页面退出了忘记注销,导致内存泄漏。后来改用DisposableEffect,问题就解决了。
@Composable
fun NetworkStateListener() {
val context = LocalContext.current
DisposableEffect(context) {
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
// 处理网络变化
}
}
val filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
context.registerReceiver(receiver, filter)
// 这个lambda就是清理函数
onDispose {
context.unregisterReceiver(receiver)
}
}
}
你想想看,onDispose里的代码会在以下情况执行:
- Composable从组合中移除
- DisposableEffect的key发生变化,先清理旧的,再执行新的
注意:DisposableEffect的onDispose不一定是「页面销毁」时才触发。只要key变了,它也会执行。所以如果你的key设计不合理,可能会频繁清理和重建资源。
1.3 SideEffect:每次重组都执行的副作用
SideEffect这个名字很直白——它就是副作用。每次Composable重组时,SideEffect里的代码都会执行。
说实话,这个API我用得比较少。因为它太「原始」了,没有生命周期感知,每次重组都跑一遍。但有些场景确实需要它,比如把Compose的状态同步到非Compose的世界里。
@Composable
fun AnalyticsScreen(screenName: String) {
SideEffect {
// 每次重组都上报当前页面名
Analytics.reportScreenView(screenName)
}
// UI...
}
嗯,这里要注意:SideEffect不能用来启动协程,它就是个同步回调。如果你想在重组时做异步操作,还是得用LaunchedEffect。
1.4 rememberCoroutineScope:在Composable外启动协程
这个API解决了一个很实际的问题:有时候你需要在事件回调里启动协程,但事件回调不在Composable函数体内。
比如按钮点击后要执行网络请求,点击事件是个lambda,它不在Composable的协程作用域里。这时候就需要rememberCoroutineScope了。
@Composable
fun LoginButton() {
val scope = rememberCoroutineScope()
var loading by remember { mutableStateOf(false) }
Button(
onClick = {
scope.launch {
loading = true
try {
login()
} finally {
loading = false
}
}
},
enabled = !loading
) {
Text(if (loading) "登录中..." else "登录")
}
}
我个人习惯把rememberCoroutineScope和LaunchedEffect搭配使用。LaunchedEffect处理页面级别的异步任务,rememberCoroutineScope处理用户交互触发的异步任务。
避坑指南:千万不要在rememberCoroutineScope的协程里直接修改Compose状态而不做异常处理。我曾经因为网络请求抛异常,导致loading状态卡死,按钮永远不可点击。记得用try-finally保证状态恢复。
1.5 rememberUpdatedState:解决闭包中的「过期引用」
这个API比较微妙,但理解了之后会觉得很巧妙。它解决的是:在LaunchedEffect或协程中,你引用的外部变量可能已经「过期」了。
为什么会这样?因为Composable会重组,重组后变量值变了,但协程里捕获的还是旧值。这就是经典的闭包陷阱。
@Composable
fun TimerDisplay(onTimeout: () -> Unit) {
// 用rememberUpdatedState包装回调
val currentOnTimeout by rememberUpdatedState(onTimeout)
LaunchedEffect(Unit) {
delay(5000)
// 这里拿到的一定是最新的onTimeout
currentOnTimeout()
}
}
你看,如果不加rememberUpdatedState,5秒后执行的onTimeout可能是旧版本的。加上之后,它始终指向最新的回调。
我在项目中遇到过类似问题:一个倒计时组件,倒计时结束后要执行某个操作。但用户可能在倒计时过程中修改了操作逻辑。用rememberUpdatedState包装后,倒计时结束总能执行最新的操作。
1.6 知识体系总览
说了这么多,我们来画个图总结一下。这张SVG图展示了Compose中EffectHandler的完整体系:
1.7 总结对比
最后,我用一张表格帮你快速对比这几个API的差异:
| API | 执行时机 | 是否支持协程 | 清理回调 | 典型场景 |
|---|---|---|---|---|
| LaunchedEffect | 进入组合时,key变化时重启 | ✅ 是 | 自动取消协程 | 网络请求、定时器 |
| DisposableEffect | 进入组合时,key变化时重启 | ❌ 否 | onDispose回调 | 注册/注销监听器 |
| SideEffect | 每次重组 | ❌ 否 | 无 | 同步状态到外部 |
| rememberCoroutineScope | 创建后一直存在 | ✅ 是 | 离开组合时取消 | 事件回调中启动协程 |
| rememberUpdatedState | 每次重组更新引用 | ❌ 否 | 无 | 解决闭包过期问题 |
嗯,这一章的内容就到这里。EffectHandler是Compose里非常实用的工具集,掌握了它们,你就能在声明式UI的世界里游刃有余地处理各种副作用。记住一个原则:能用LaunchedEffect就别用SideEffect,能用DisposableEffect就别手动管理资源。
公众号:蓝海资料掘金营,微信deep3321