第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的完整体系:

Compose EffectHandler 知识体系 EffectHandler LaunchedEffect DisposableEffect SideEffect rememberCoroutineScope rememberUpdatedState 虚线表示 rememberUpdatedState 通常与其他 Effect 配合使用 协程启动 资源清理 同步回调 外部协程 闭包更新

1.7 总结对比

最后,我用一张表格帮你快速对比这几个API的差异:

API 执行时机 是否支持协程 清理回调 典型场景
LaunchedEffect 进入组合时,key变化时重启 ✅ 是 自动取消协程 网络请求、定时器
DisposableEffect 进入组合时,key变化时重启 ❌ 否 onDispose回调 注册/注销监听器
SideEffect 每次重组 ❌ 否 同步状态到外部
rememberCoroutineScope 创建后一直存在 ✅ 是 离开组合时取消 事件回调中启动协程
rememberUpdatedState 每次重组更新引用 ❌ 否 解决闭包过期问题

嗯,这一章的内容就到这里。EffectHandler是Compose里非常实用的工具集,掌握了它们,你就能在声明式UI的世界里游刃有余地处理各种副作用。记住一个原则:能用LaunchedEffect就别用SideEffect,能用DisposableEffect就别手动管理资源

公众号:蓝海资料掘金营,微信deep3321