先提前划重点:Compose的核心思想是"状态驱动UI",而副作用是"脱离UI渲染、影响外部环境的操作",Compose提供的副作用API,本质上是让这些操作"跟随Composable的生命周期",避免内存泄漏、重复执行等问题。
一、先搞懂:什么是Compose的副作用?
在讲具体API之前,我们必须先明确"副作用"的定义------不是什么高深的概念,用一句话就能说清楚:
副作用(Side Effect):在Composable函数中,除了"根据状态渲染UI"之外,所有影响外部环境(或被外部环境影响)的操作,都是副作用。
举几个初学者最容易接触到的副作用场景,一看就懂:
- 网络请求(比如调用接口获取数据,影响了服务器,也被服务器返回的数据影响);
- 日志打印(输出内容到控制台,影响了外部的日志系统);
- 操作数据库(增删改查,影响了本地存储);
- 注册/注销监听器(比如监听网络状态、传感器,影响了系统服务);
- 启动协程、延迟操作(比如delay(2000),影响了线程调度);
- 修改全局变量(比如修改一个单例中的属性,影响了函数外部的状态)。
那为什么不能直接在@Composable函数里写这些操作?比如下面这段代码,初学者很容易这么写,但其实是错误的:
kotlin
@Composable
fun BadExample() {
// 错误:直接在Composable中执行副作用(网络请求)
val data = api.fetchData() // 网络请求,属于副作用
Text(text = data.content)
}
原因很简单:Composable函数的执行是"不可预测"的------它可能会被多次重组(Recompose)、被取消、在后台线程执行,甚至重复执行。如果直接在里面写副作用,会导致:
- 重复执行:比如重组一次,就发起一次网络请求,造成资源浪费;
- 内存泄漏:比如注册了监听器,但没有注销,Composable销毁后监听器还在,导致内存泄漏;
- 状态混乱:比如协程还在执行,Composable已经重组,导致UI状态和协程执行结果不一致。
所以,Compose官方提供了一系列"副作用API",目的就是:让副作用操作"跟随Composable的生命周期",确保副作用只在合适的时机执行、在合适的时机清理,避免上述问题。
这里先补充一个基础知识点(很重要):Composable的生命周期只有3个阶段,副作用API都是围绕这3个阶段设计的:
- 进入组合(Enter Composition):Composable首次被调用,加入到UI树中;
- 重组(Recompose):Composable依赖的状态发生变化,重新执行函数、更新UI(可能执行0次或多次);
- 退出组合(Leave Composition):Composable不再被使用(比如页面跳转、if判断为false),从UI树中移除。
所有副作用API,本质上都是"绑定"到这3个阶段,控制副作用的执行和清理时机。接下来,我们逐个解析最常用的5个副作用API,每个都讲透"是什么、怎么用、适用场景、避坑点"。
二、核心副作用API详解(逐个拆解,含完整示例)
我们按照"初学者使用频率"排序,从最常用的LaunchedEffect开始,逐个讲解,每个API都配套可直接复制运行的代码示例,以及详细的注释说明。
1. LaunchedEffect:最常用的"协程副作用"(必学)
1.1 是什么?
LaunchedEffect是Compose中最常用的副作用API,核心作用是:在Composable的作用域内,启动一个协程,并且让协程的生命周期和Composable完全绑定。
简单说:当Composable进入组合时,LaunchedEffect会启动内部的协程;当Composable退出组合时,协程会被自动取消;当LaunchedEffect的"key"发生变化时,旧协程会被取消,新协程会重新启动。
它的核心优势是:无需手动管理协程的生命周期,避免协程泄漏(比如Composable销毁了,协程还在执行)。
1.2 基本用法(语法)
scss
LaunchedEffect(keys = [key1, key2, ...]) {
// 这里可以写挂起函数(suspend),比如网络请求、delay、Flow收集等
// 协程会在Composable进入组合时启动,退出时自动取消
}
关键参数说明(初学者重点关注):
- keys(可变参数):"触发重启"的关键。当keys中的任意一个值发生变化时,LaunchedEffect会取消当前正在运行的协程,重新启动一个新的协程;
- 协程块:里面可以执行任何挂起函数(suspend),这是LaunchedEffect和其他副作用API的核心区别(专门用于协程相关的副作用)。
1.3 3个常用场景(含完整示例)
场景1:页面首次进入时,发起一次网络请求(最常用场景)
kotlin
@Composable
fun UserProfileScreen(userId: String, viewModel: UserViewModel) {
// 状态:用于存储请求到的用户数据
val userState by viewModel.userData.collectAsState(initial = null)
val loadingState by viewModel.loading.collectAsState(initial = false)
val errorState by viewModel.error.collectAsState(initial = null)
// 副作用:页面进入时,根据userId请求用户数据
// keys = [userId]:当userId变化时,取消旧请求,发起新请求
LaunchedEffect(key1 = userId) {
// 启动协程,执行挂起函数(viewModel.fetchUser是挂起函数)
viewModel.fetchUser(userId)
}
// 根据状态渲染UI(无副作用,纯渲染)
when {
loadingState -> CircularProgressIndicator() // 加载中
errorState != null -> Text(text = "请求失败:${errorState}") // 错误提示
userState != null -> {
// 渲染用户信息
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(text = "用户名:${userState?.name}", fontSize = 18.sp)
Text(text = "手机号:${userState?.phone}")
Text(text = "邮箱:${userState?.email}")
}
}
}
}
// ViewModel中的挂起函数(示例)
class UserViewModel : ViewModel() {
private val _userData = MutableStateFlow<User?>(null)
val userData: StateFlow<User?> = _userData.asStateFlow()
private val _loading = MutableStateFlow(false)
val loading: StateFlow<Boolean> = _loading.asStateFlow()
private val _error = MutableStateFlow<String?>(null)
val error: StateFlow<String?> = _error.asStateFlow()
// 挂起函数:发起网络请求
suspend fun fetchUser(userId: String) {
_loading.value = true
_error.value = null
try {
// 模拟网络请求(delay是挂起函数)
delay(1500)
val user = apiService.getUserById(userId) // 假设apiService是网络请求工具
_userData.value = user
} catch (e: Exception) {
_error.value = e.message
} finally {
_loading.value = false
}
}
}
示例说明:
- 当UserProfileScreen首次进入组合时,LaunchedEffect会启动协程,调用viewModel.fetchUser(userId)发起网络请求;
- 如果userId发生变化(比如从"123"变成"456"),LaunchedEffect会取消之前的协程(如果请求还在进行),重新启动协程,请求新的用户数据;
- 当页面跳转(UserProfileScreen退出组合),协程会被自动取消,避免请求完成后更新已销毁的UI,也避免内存泄漏。
场景2:一次性副作用(只执行一次,不随重组重启)
如果我们希望副作用只在Composable首次进入组合时执行一次,后续重组不重启,只需要将keys设为Unit(Unit是一个常量,永远不会变化)。
scss
@Composable
fun SplashScreen(onNavigateToHome: () -> Unit) {
// 副作用:只执行一次,延迟2秒后跳转到首页
LaunchedEffect(key1 = Unit) {
delay(2000) // 模拟启动页加载(挂起函数)
onNavigateToHome() // 跳转首页
}
// 渲染启动页UI
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(text = "欢迎使用", fontSize = 24.sp, fontWeight = FontWeight.Bold)
}
}
示例说明:keys = Unit,意味着LaunchedEffect只会在SplashScreen首次进入组合时启动一次协程,后续无论SplashScreen是否重组(比如父组件重组),协程都不会重新启动,完美实现"一次性启动页延迟跳转"。
场景3:监听Flow并更新UI(协程收集Flow)
在Compose中,收集Flow也是常见的副作用,通常用LaunchedEffect来启动协程收集,避免手动管理协程。
scss
@Composable
fun MessageScreen() {
// 状态:存储消息列表
val messages = remember { mutableStateListOf<Message>() }
// 模拟一个Flow(比如从数据库或WebSocket获取消息)
val messageFlow = remember { flow {
repeat(5) {
delay(1000)
emit(Message(content = "消息${it+1}", time = System.currentTimeMillis()))
}
} }
// 副作用:收集Flow,将消息添加到列表中
LaunchedEffect(key1 = messageFlow) {
messageFlow.collect { message ->
messages.add(message)
}
}
// 渲染消息列表
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(messages) { message ->
Text(
text = "${message.content}(${message.time})",
modifier = Modifier.padding(16.dp)
)
}
}
}
// 消息数据类
data class Message(val content: String, val time: Long)
示例说明:LaunchedEffect启动协程,收集messageFlow,每收到一条消息就添加到messages列表中,UI会自动重组更新;当MessageScreen退出组合时,协程会被取消,Flow收集也会停止,避免内存泄漏。
1.4 初学者避坑点
- 坑1:在LaunchedEffect中修改非状态变量 → 导致UI不更新。比如在协程中直接修改普通变量,而不是MutableState或StateFlow,UI不会感知到变化;
- 坑2:keys参数传错 → 比如不传keys(默认是emptyArray),会导致每次重组都重启协程;或者传了一个频繁变化的参数(比如Int类型的count),导致协程频繁取消和重启;
- 坑3:在LaunchedEffect中执行非挂起函数 → 虽然可以,但没必要。LaunchedEffect的核心价值是管理协程和挂起函数,非挂起函数(比如普通日志打印)可以用其他更合适的副作用API。
2. DisposableEffect:需要"手动清理"的副作用(必学)
2.1 是什么?
DisposableEffect和LaunchedEffect类似,也是用于执行副作用,但它的核心场景是:副作用操作需要"手动清理" ------比如注册监听器、绑定系统服务、订阅广播等,这些操作如果不手动清理,会导致内存泄漏。
DisposableEffect的特点:
- 进入组合时,执行副作用操作;
- keys变化时,先执行"清理操作",再执行新的副作用操作;
- 退出组合时,必须执行"清理操作"(编译器强制要求,不写会报错)。
注意:DisposableEffect不能执行挂起函数(这是和LaunchedEffect的核心区别),如果需要执行挂起函数,需要配合rememberCoroutineScope使用。
2.2 基本用法(语法)
scss
DisposableEffect(keys = [key1, key2, ...]) {
// 1. 执行副作用操作(比如注册监听器、绑定服务)
val listener = MyListener()
systemService.registerListener(listener)
// 2. 必须返回一个清理函数(onDispose),编译器强制要求
onDispose {
// 清理操作:比如注销监听器、解绑服务
systemService.unregisterListener(listener)
}
}
关键说明:
- keys参数:和LaunchedEffect一样,keys变化时,会先执行onDispose清理,再重新执行副作用;
- onDispose函数:必须返回,这是DisposableEffect的核心,用于清理副作用产生的资源,避免内存泄漏;
- 不能写挂起函数:如果需要在DisposableEffect中执行挂起操作,需要先通过rememberCoroutineScope获取协程作用域,再在作用域中启动协程。
2.3 2个常用场景(含完整示例)
场景1:注册/注销位置监听器(最典型场景)
kotlin
@Composable
fun LocationTrackingScreen(userId: String) {
val context = LocalContext.current
// 状态:存储当前位置
val currentLocation = remember { mutableStateOf<Location?>(null) }
// 获取系统位置服务
val locationManager = remember {
context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
}
// 副作用:注册位置监听器,退出时注销
DisposableEffect(key1 = userId) {
// 1. 创建监听器
val locationListener = object : LocationListener {
override fun onLocationChanged(location: Location) {
// 位置更新,修改状态(触发UI重组)
currentLocation.value = location
// 可选:将位置上传到服务器(非挂起操作)
uploadLocation(userId, location)
}
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}
override fun onProviderEnabled(provider: String) {}
override fun onProviderDisabled(provider: String) {}
}
// 2. 注册监听器(副作用操作)
if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
5000L, // 5秒更新一次
10f, // 移动10米更新一次
locationListener
)
Log.d("Location", "为用户$userId 注册位置监听器")
}
// 3. 清理操作:注销监听器(必须写)
onDispose {
locationManager.removeUpdates(locationListener)
Log.d("Location", "为用户$userId 注销位置监听器")
}
}
// 渲染UI
Column(modifier = Modifier.padding(16.dp)) {
Text(text = "用户$userId 的位置跟踪", fontSize = 18.sp, fontWeight = FontWeight.Bold)
if (currentLocation.value != null) {
Text(text = "当前经度:${currentLocation.value?.longitude}")
Text(text = "当前纬度:${currentLocation.value?.latitude}")
} else {
Text(text = "正在获取位置...")
}
}
}
// 模拟位置上传(非挂起函数)
fun uploadLocation(userId: String, location: Location) {
// 这里可以写网络请求(非挂起,比如用OkHttp同步请求,或另起协程)
Log.d("Location", "上传用户$userId 位置:${location.latitude}, ${location.longitude}")
}
示例说明:
- 当LocationTrackingScreen进入组合时,DisposableEffect会注册位置监听器,开始获取位置;
- 当userId变化时,会先执行onDispose注销之前的监听器,再为新的userId注册新的监听器;
- 当页面退出组合时,onDispose会自动执行,注销监听器,避免内存泄漏(如果不注销,locationManager会一直持有监听器引用,导致Composable无法被回收)。
场景2:监听Activity生命周期(结合LocalLifecycleOwner)
在Compose中,我们可以通过DisposableEffect监听传统Activity的生命周期(比如onStart、onStop),需要配合LocalLifecycleOwner使用。
kotlin
@Composable
fun LifecycleListenerScreen() {
// 获取当前的LifecycleOwner(通常是宿主Activity/Fragment)
val lifecycleOwner = LocalLifecycleOwner.current
// 副作用:监听生命周期,退出时移除监听器
DisposableEffect(key1 = lifecycleOwner) {
// 创建生命周期监听器
val lifecycleObserver = object : LifecycleEventObserver {
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
// 根据生命周期事件做操作
when (event) {
Lifecycle.Event.ON_START -> Log.d("Lifecycle", "Activity 进入前台")
Lifecycle.Event.ON_STOP -> Log.d("Lifecycle", "Activity 进入后台")
Lifecycle.Event.ON_DESTROY -> Log.d("Lifecycle", "Activity 销毁")
else -> {}
}
}
}
// 注册监听器
lifecycleOwner.lifecycle.addObserver(lifecycleObserver)
// 清理操作:移除监听器
onDispose {
lifecycleOwner.lifecycle.removeObserver(lifecycleObserver)
Log.d("Lifecycle", "移除生命周期监听器")
}
}
// 渲染UI
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(text = "监听Activity生命周期", fontSize = 20.sp)
}
}
示例说明:
- LocalLifecycleOwner.current获取当前Composable的宿主生命周期所有者(比如Activity);
- DisposableEffect注册生命周期监听器,监听Activity的ON_START、ON_STOP等事件;
- 退出组合时,onDispose移除监听器,避免内存泄漏。
2.4 初学者避坑点
- 坑1:忘记写onDispose → 编译器会直接报错,这是DisposableEffect的强制要求,即使没有清理操作,也要写空的onDispose {};
- 坑2:在DisposableEffect中执行挂起函数 → 会报错,因为DisposableEffect的代码块是普通函数,不是挂起函数;如果需要执行挂起操作,需要配合rememberCoroutineScope;
- 坑3:keys参数传错 → 比如传了一个频繁变化的参数,导致频繁注册/注销,影响性能(比如传count,每次count变化都要重新注册监听器)。
3. SideEffect:每次重组后执行的副作用(简单但常用)
3.1 是什么?
SideEffect是最"简单"的副作用API,核心作用是:在每次Composable重组成功后,执行一次副作用操作。
它的特点:
- 没有keys参数,每次重组成功(UI更新完成)后都会执行;
- 不需要手动清理(适合不需要清理的简单副作用);
- 不能执行挂起函数;
- 执行时机:重组完成后,UI已经更新到屏幕上。
适用场景:简单的日志打印、埋点统计、将Compose状态同步到非Compose代码(比如传统View、单例类)。
3.2 基本用法(语法)
arduino
SideEffect {
// 执行副作用操作(非挂起、无需清理)
// 每次重组成功后都会执行
Log.d("SideEffect", "Composable重组完成")
analytics.trackScreenView("HomeScreen") // 埋点统计
}
3.3 常用场景(含完整示例)
场景:埋点统计(每次重组都更新当前页面的埋点信息)
kotlin
// 模拟一个非Compose的埋点工具类(传统代码)
object AnalyticsTracker {
// 记录当前页面的步骤(比如注册流程的步骤)
fun trackRegistrationStep(step: Int, isCompleted: Boolean) {
Log.d("Analytics", "注册步骤:$step,是否完成:$isCompleted")
// 实际开发中,这里会调用埋点SDK(比如友盟、百度统计)
}
}
@Composable
fun RegistrationScreen() {
// 状态:当前注册步骤(1-3)
var currentStep by remember { mutableStateOf(1) }
// 状态:当前步骤是否完成
var isStepCompleted by remember { mutableStateOf(false) }
// 副作用:每次重组成功后,上报当前步骤信息(埋点)
SideEffect {
AnalyticsTracker.trackRegistrationStep(currentStep, isStepCompleted)
}
// 渲染注册步骤UI
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "注册步骤 $currentStep", fontSize = 20.sp, fontWeight = FontWeight.Bold)
Button(onClick = {
// 模拟完成当前步骤
isStepCompleted = true
}) {
Text(text = "完成当前步骤")
}
Button(
onClick = {
if (currentStep < 3) {
currentStep++
isStepCompleted = false
}
},
enabled = isStepCompleted
) {
Text(text = "进入下一步")
}
}
}
示例说明:
- 当currentStep或isStepCompleted变化时,Composable会重组;
- 重组成功后,SideEffect会自动执行,调用AnalyticsTracker.trackRegistrationStep上报当前步骤信息;
- 不需要清理操作,因为埋点操作是"一次性"的,不会产生需要回收的资源。
3.4 初学者避坑点
- 坑1:在SideEffect中执行需要清理的操作 → 比如注册监听器,会导致内存泄漏(SideEffect没有onDispose);
- 坑2:在SideEffect中执行耗时操作 → 会阻塞UI线程,因为SideEffect在主线程执行,且每次重组都会执行;
- 坑3:误以为SideEffect只执行一次 → 其实每次重组成功都会执行,比如点击按钮修改状态,导致重组,SideEffect就会再执行一次。
4. rememberCoroutineScope:手动控制的协程作用域(补充API)
4.1 是什么?
rememberCoroutineScope不是"副作用API",但它和副作用密切相关,核心作用是:获取一个和Composable生命周期绑定的协程作用域(CoroutineScope) ,用于手动启动协程(比如用户点击事件触发的协程)。
它和LaunchedEffect的区别(重点,初学者容易混淆):
| 特性 | LaunchedEffect | rememberCoroutineScope |
|---|---|---|
| 启动时机 | Composable进入组合时,自动启动协程 | 手动调用scope.launch,按需启动协程(比如点击事件) |
| 协程管理 | 自动管理生命周期(进入启动、退出取消、key变化重启) | 手动管理协程(需手动cancel,但Composable退出时会自动取消作用域) |
| 适用场景 | 自动执行的协程(页面进入请求数据、Flow收集) | 用户交互触发的协程(点击按钮发起请求、延迟提示) |
4.2 基本用法(语法)
scss
// 获取协程作用域(和Composable生命周期绑定)
val scope = rememberCoroutineScope()
// 手动启动协程(比如点击事件中)
Button(onClick = {
scope.launch {
// 执行挂起函数
delay(1000)
showToast(context, "点击成功")
}
}) {
Text(text = "点击我")
}
4.3 常用场景(含完整示例)
场景:用户点击按钮,延迟1秒显示Toast(手动触发协程)
kotlin
@Composable
fun DelayedToastButton() {
val context = LocalContext.current
// 获取协程作用域(和当前Composable生命周期绑定)
val scope = rememberCoroutineScope()
Button(
onClick = {
// 手动启动协程(用户点击时触发)
scope.launch {
// 执行挂起函数(延迟1秒)
delay(1000)
// 显示Toast(非挂起操作)
Toast.makeText(context, "点击成功!", Toast.LENGTH_SHORT).show()
}
},
modifier = Modifier.padding(16.dp)
) {
Text(text = "点击我,1秒后显示Toast")
}
}
示例说明:
- rememberCoroutineScope获取的scope,会和当前Composable的生命周期绑定;
- 用户点击按钮时,手动调用scope.launch启动协程,延迟1秒后显示Toast;
- 如果Composable在协程执行过程中退出组合(比如页面跳转),scope会被自动取消,协程也会停止,避免Toast在页面销毁后还显示。
4.4 初学者避坑点
- 坑1:用GlobalScope替代rememberCoroutineScope → GlobalScope的协程不与任何Composable绑定,Composable退出后协程还会继续执行,容易导致内存泄漏;
- 坑2:忘记手动取消协程 → 虽然scope会在Composable退出时自动取消,但如果需要在某个条件下手动取消(比如用户再次点击),需要保存协程的Job,调用job.cancel();
- 坑3:在rememberCoroutineScope中启动的协程,没有和keys绑定 → 如果需要根据状态变化取消协程,需要手动管理(比如用LaunchedEffect更合适)。
5. rememberUpdatedState:获取最新状态的"保鲜剂"(进阶API)
5.1 是什么?
rememberUpdatedState是一个"辅助API",核心作用是:在副作用中,获取状态的"最新值",即使副作用没有被重启。
举个例子:如果我们用LaunchedEffect启动一个长期运行的协程(比如循环执行的动画),协程中需要用到某个状态,但我们不希望这个状态变化时,协程被取消重启(因为重启会中断动画),这时候就可以用rememberUpdatedState来获取状态的最新值。
简单说:rememberUpdatedState可以"保鲜"状态的最新值,让副作用在不重启的情况下,使用最新的状态。
5.2 基本用法(语法)
scss
// 原始状态(可能会变化)
var state by remember { mutableStateOf(initialValue) }
// 用rememberUpdatedState包裹,获取最新值
val updatedState = rememberUpdatedState(state)
// 在副作用中使用updatedState.value(始终是最新值)
LaunchedEffect(Unit) {
while (true) {
delay(1000)
Log.d("UpdatedState", "最新状态:${updatedState.value}")
}
}
5.3 常用场景(含完整示例)
场景:长期运行的动画,根据状态变化调整动画速度(不重启协程)
ini
@Composable
fun PulseAnimation() {
// 状态:动画间隔(用户可以调整,比如3000ms → 1000ms)
var pulseInterval by remember { mutableStateOf(3000L) }
// 状态:动画透明度(0f → 1f → 0f 循环)
val alpha = remember { Animatable(1f) }
// 用rememberUpdatedState包裹pulseInterval,获取最新值
val updatedPulseInterval = rememberUpdatedState(pulseInterval)
// 副作用:启动长期运行的协程(循环执行动画)
// keys = Unit:只启动一次,不随pulseInterval变化而重启
LaunchedEffect(Unit) {
while (isActive) { // isActive:协程是否活跃
// 使用updatedPulseInterval.value(始终是最新的间隔时间)
delay(updatedPulseInterval.value)
// 执行动画(挂起函数)
alpha.animateTo(0f, animationSpec = tween(500))
alpha.animateTo(1f, animationSpec = tween(500))
}
}
// 渲染UI:动画文本 + 调整间隔的按钮
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "脉冲动画",
fontSize = 24.sp,
modifier = Modifier.graphicsLayer { this.alpha = alpha.value }
)
Row(
modifier = Modifier.padding(top = 32.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Button(onClick = { pulseInterval = 3000L }) {
Text(text = "慢节奏(3秒)")
}
Button(onClick = { pulseInterval = 1000L }) {
Text(text = "快节奏(1秒)")
}
}
}
}
示例说明:
- LaunchedEffect的keys = Unit,意味着协程只启动一次,不会随pulseInterval变化而重启;
- 用rememberUpdatedState包裹pulseInterval,在协程中使用updatedPulseInterval.value,即使pulseInterval变化,协程也不会重启,但能获取到最新的间隔时间;
- 这样既保证了动画的连续性(不被重启中断),又能根据用户操作调整动画速度,完美解决"既要最新状态,又不重启副作用"的需求。
5.4 初学者避坑点
- 坑1:滥用rememberUpdatedState → 只有当"副作用不能重启,但需要获取最新状态"时才用,否则用普通状态即可;
- 坑2:忘记用.value获取值 → rememberUpdatedState返回的是一个State对象,必须通过.value才能获取到最新值;
- 坑3:在rememberUpdatedState中使用不可变状态 → 没有意义,因为不可变状态不会变化,不需要"保鲜"。
三、总结:5个副作用API对比(初学者快速查阅)
为了方便大家记忆和快速选择合适的API,我整理了一张对比表,涵盖核心特点、适用场景和关键注意点:
| API名称 | 核心特点 | 适用场景 | 关键注意点 |
|---|---|---|---|
| LaunchedEffect | 自动启动协程,绑定Composable生命周期,支持keys重启 | 网络请求、Flow收集、动画、一次性延迟操作 | 可执行挂起函数,keys决定是否重启 |
| DisposableEffect | 需手动清理,支持keys重启,编译器强制要求onDispose | 注册/注销监听器、绑定/解绑系统服务 | 不能执行挂起函数,必须写onDispose |
| SideEffect | 每次重组成功后执行,无需清理,无keys | 埋点统计、日志打印、同步状态到非Compose代码 | 不能执行挂起函数,每次重组都执行 |
| rememberCoroutineScope | 获取协程作用域,手动启动协程,绑定生命周期 | 用户交互触发的协程(点击按钮、手势) | 不要用GlobalScope替代,需手动管理协程 |
| rememberUpdatedState | 保鲜状态最新值,不重启副作用 | 长期运行的副作用,需要获取最新状态但不重启 | 必须通过.value获取值,不滥用 |
四、初学者实战建议(必看)
看完上面的解析,可能有初学者会觉得"知识点太多,记不住",这里给大家3个实战建议,帮助大家快速上手:
- 先掌握核心3个API:LaunchedEffect、DisposableEffect、SideEffect,这3个覆盖了80%的开发场景,rememberCoroutineScope和rememberUpdatedState可以后续再深入;
- 写代码时,先判断"是否是副作用":如果是网络请求、协程、监听器、日志,就用对应的副作用API,不要直接写在Composable函数体中;
- 多练多踩坑:把上面的示例代码复制到项目中,运行起来,修改参数(比如keys、状态值),观察副作用的执行和清理时机,比单纯看理论更有效。
最后提醒一句:Compose的副作用API,核心都是"让副作用跟随Composable的生命周期",只要记住这一点,就能避免大部分坑。比如:需要协程 → LaunchedEffect;需要清理 → DisposableEffect;简单打印 → SideEffect;手动触发 → rememberCoroutineScope。
如果大家在使用过程中遇到具体问题(比如协程泄漏、副作用不执行),可以在评论区留言,我会一一解答。
好了,今天的Jetpack Compose副作用解析就到这里,希望这篇超详细的博客能帮助初学者快速掌握副作用的使用,少走弯路。后续我还会分享更多Compose的实战技巧,记得关注哦!