上一章中了解了可组合项的生命周期和因状态变化重组,而附带效应(Side Effects)正是解决 “重组时如何安全执行界面描述之外的操作” 的核心方案,比如网络请求、日志上报、生命周期监听等。
关键术语:效应(Effect)本身是一种可组合函数,它不输出界面,且只在组合完成后产生附带效应,比如 LaunchedEffect、DisposableEffect 都是效应。
本章的示例代码是伪代码只是展示使用场景,部分可运行...
官方文档:Compose 官方文档
一、什么是附带效应?
有一个大前提:可组合项理想状态下应该 “无附带效应”。因为可组合项的核心作用是 “描述界面”,而附带效应是 “发生在可组合函数作用域之外的应用状态变化”,比如:
- 发送网络请求、保存数据到本地;
- 显示 Snackbar、Toast 等临时提示;
- 注册 / 取消生命周期监听、日志上报。
这些操作之所以不能直接写在可组合项里,是因为可组合项的重组有 “不可预测性”:
- 重组可能频繁触发(比如状态快速变化时);
- 重组顺序可能和首次执行不同;
- 部分重组可能被 Compose 舍弃(比如还没执行完就触发新重组)。
如果直接在可组合项里写这些操作,会导致重复请求、监听泄漏、状态混乱等问题。因此,Compose 提供了专门的 “效应 API”,让这些操作能在 “感知可组合项生命周期的受控环境” 中执行 —— 简单说就是 “该执行时执行,该取消时取消”。
二、7 个核心效应 API
1. LaunchedEffect:在可组合项内运行挂起函数
适用场景
需要在可组合项的生命周期内执行挂起函数(如 delay、网络请求、动画),且希望函数能随可组合项 “进入而启动、退出而取消”。
核心逻辑
- 当
LaunchedEffect进入组合时,启动一个协程执行代码块; - 当
LaunchedEffect退出组合时,自动取消协程; - 若传入的 “键(key)” 变化,会先取消当前协程,再启动新协程执行代码块(即 “重启效应”)。
示例代码 - 实现 Alpha 脉冲动画
@Composable
fun PulseAnimation() {
// 可配置的脉冲间隔(状态变化时会触发效应重启)
var pulseRateMs by remember { mutableStateOf(3000L) }
// 动画状态:初始透明度1f(不透明)
val alphaAni = remember { Animatable(1f) }
// 键为pulseRateMs:当间隔变化时,重启动画协程
LaunchedEffect(pulseRateMs) {
// isActive是协程的状态:效应取消时为false,循环终止
while (isActive) {
delay(pulseRateMs) // 等待设定间隔
alphaAni.animateTo(0f) // 透明度降到0(完全透明)
alphaAni.animateTo(1f) // 透明度恢复到1(不透明)
}
}
// 使用动画透明度绘制文本
Text(
text = "脉冲动画",
modifier = Modifier.size(200.dp).alpha(alpha = alphaAni.value),
fontSize = 24.sp,
)
}
2. rememberCoroutineScope:在可组合项外启动协程
适用场景
LaunchedEffect是可组合函数,只能在其他可组合项内使用;如果需要在 “可组合项外” 启动协程(如按钮点击事件、回调函数),就用rememberCoroutineScope;- 需要手动控制协程生命周期(如手动取消动画、中断请求)。
核心逻辑
rememberCoroutineScope是可组合函数,返回一个绑定到 “当前组合点” 的CoroutineScope;- 当可组合项退出组合时,该作用域会自动取消,所有在该作用域下启动的协程也会被取消。
示例代码 - 点击按钮显示 Snackbar
@Composable
fun MoviesScreen() {
// 创建与MoviesScreen生命周期绑定的协程作用域
val scope = rememberCoroutineScope()
// Snackbar的状态宿主
val snackbarHostState = remember { SnackbarHostState() }
Scaffold(
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
) { contentPadding ->
Column(Modifier.padding(contentPadding)) {
Button(onClick = {
// 在按钮点击事件(可组合项外)启动协程
scope.launch {
snackbarHostState.showSnackbar(
message = "已加载最新电影列表",
actionLabel = "知道了"
)
}
}) {
Text("加载最新网络电影")
}
}
}
}
3. rememberUpdatedState:效应中引用值但不重启
适用场景
- 当
LaunchedEffect等效应依赖某个值,但 “该值变化时不希望效应重启”—— 比如长期运行的任务(如倒计时),中途依赖的回调函数更新了,不想让倒计时重新开始。
核心逻辑
rememberUpdatedState会创建一个State对象,始终持有该值的 “最新版本”;- 效应中引用这个
State对象的值,而非直接引用原变量,这样即使原变量变化,效应也不会重启,但能拿到最新值。
示例代码 - 倒计时
@Composable
fun LandingScreen(onTimeout: () -> Unit) {
// rememberUpdatedState可以确保重组LandingScreen时:
// onTimeout始终包含重组使用的最新值,且不触发效应重启
val currentOnTimeout by rememberUpdatedState(onTimeout)
// 键为true:效应只在进入组合时启动一次,不随任何值重启
LaunchedEffect(true) {
delay(3000L) // 倒计时3秒
currentOnTimeout() // 执行回调
}
Box(modifier = Modifier.fillMaxSize()) {
Text(
text = "rememberUpdatedState 示例",
modifier = Modifier.align(Alignment.Center),
fontSize = 28.sp
)
}
}
// 在需要的地方使用,3s 后会执行回调内容
LandingScreen() {
Toast.makeText(context, "这是一条消息", Toast.LENGTH_SHORT).show()
}
不过 LaunchedEffect(true) 官方有个警告,意思大概就是要谨慎使用,因为使用了这个效应就只启动一次,除非可组合项退出再进入才会再次启动,可能会导致状态不一致。

4. DisposableEffect:需要清理的效应
适用场景
- 当效应需要 “清理操作” 时 —— 比如注册监听后要取消监听、打开资源后要关闭资源,否则会导致内存泄漏。
核心逻辑
- 行为和
LaunchedEffect类似:键变化时重启,退出组合时清理; - 强制要求在代码块最后添加
onDispose子句,用于编写清理逻辑(否则 IDE 会报错)。
示例代码 - 监听生命周期事件
@Composable
fun HomeScreen(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
onStart: () -> Unit, // 页面启动时的分析上报
onStop: () -> Unit // 页面停止时的分析上报
) {
// 用rememberUpdatedState确保拿到最新的回调
val currentOnStart by rememberUpdatedState(onStart)
val currentOnStop by rememberUpdatedState(onStop)
// 键为lifecycleOwner:当生命周期所有者变化时,重启效应(重新注册监听)
DisposableEffect(lifecycleOwner) {
// 创建生命周期观察者
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_START -> currentOnStart()
Lifecycle.Event.ON_STOP -> currentOnStop()
else -> {}
}
}
// 注册观察者
lifecycleOwner.lifecycle.addObserver(observer)
// 退出组合时取消注册
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
// 其它的页面内容
Text(text = "首页内容", fontSize = 24.sp)
}
// 在需要的地方
HomeScreen(
onStart = {
Log.d("MainActivity", "onStart 启动")
},
onStop = {
Log.d("MainActivity", "onStop 停止")
}
)
onDispose 的代码块可以为空,只不过这样用可能会存在某种更适合该使用场景的效应。
5. SideEffect:将 Compose 状态同步到非 Compose 代码
适用场景
- 需要将 Compose 管理的状态(如用户信息、主题设置)同步到 “非 Compose 代码”(如第三方 SDK、原生 View、单例类),且确保每次重组成功后都同步。
核心逻辑
SideEffect会在 “每次成功重组后” 执行代码块;- 避免直接在可组合项中同步状态(因为重组可能被舍弃,导致同步不及时或重复)。
示例代码 - 将用户类型传递到外部的分析库用于细分用户群体
@Composable
fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics {
// 记住Firebase实例(避免每次重组都创建)
val analytics = remember { FirebaseAnalytics.getInstance(context) }
// 每次重组成功后,更新用户属性到Firebase
SideEffect {
analytics.setUserProperty("userType", user.userType) // userType是Compose状态
}
return analytics
}
6. produceState:将非 Compose 状态转为 Compose 状态
适用场景
- 需要将 “非 Compose 管理的状态”(如 Flow、LiveData、RxJava、网络请求结果)转换为 Compose 能识别的
State,以便在界面中使用。
核心逻辑
produceState会启动一个协程,作用域绑定到组合;- 初始值由
initialValue指定,value = 新值更新状态,相同的值或返回的State有冲突不会触发重组; - 键变化时取消当前协程,重启并重新获取状态;
- 退出组合时取消协程,自动清理资源。
示例代码 - 从网络加载图片(转为 State)
// 从网络加载图片,返回Compose的State<Result<Image>>
@Composable
fun loadNetworkImage(
url: String,
imageRepository: ImageRepository = ImageRepository()
): State<Result<Image>> {
// 初始状态为Loading,键为url和imageRepository(任一变化则重新加载)
return produceState(
initialValue = Result.Loading,
key1 = url,
key2 = imageRepository
) {
// 协程中执行挂起函数(加载图片)
val image = imageRepository.loadImage(url)
// 更新状态:成功则传图片,失败则传Error
value = if (image != null) {
Result.Success(image)
} else {
Result.Error("图片加载失败")
}
}
}
// 结果密封类:表示加载状态
sealed class Result<out T> {
object Loading : Result<Nothing>()
data class Success<out T>(val data: T) : Result<T>()
data class Error(val message: String) : Result<Nothing>()
}
7. derivedStateOf:将一个或多个状态对象转换为其他状态(降低重组频率 - 状态派生优化)
适用场景
- 当某个状态是由其他状态派生而来,但 “原状态变化频率远高于界面需要的更新频率”—— 比如列表滚动时,
firstVisibleItemIndex每秒变化多次,但界面只需要在 “索引> 0 时更新显示回到顶部按钮即可,不需要频繁观察当前第一项索引是否 > 0了”。
核心逻辑
derivedStateOf创建一个新的State,只有当 “派生结果变化” 时才触发重组;- 类似 Kotlin Flow 的
distinctUntilChanged(),避免不必要的重组(比如索引从 1→2→3,派生结果都是 “显示按钮”,不会触发重组)。
示例代码 - 滚动列表显示回到顶部按钮
@Composable
fun MessageList(names: List<String>) {
Box {
// 记住LazyColumn的滚动状态
val listState = rememberLazyListState()
// 列表:显示所有信息
LazyColumn(state = listState) {
items(names) { name ->
Text(text = name, modifier = Modifier.padding(8.dp))
}
}
// 派生状态:只有当第一个可见项索引>0时,才显示回到顶部按钮
val showTopButton by remember {
derivedStateOf { listState.firstVisibleItemIndex > 0 }
}
// 按钮:显示/隐藏由showTopButton控制
AnimatedVisibility(visible = showTopButton) {
Button(
onClick = {
// 点击回到顶部
CoroutineScope(Dispatchers.Main).launch {
listState.animateScrollToItem(0)
}
},
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(16.dp)
) {
Text("回到顶部")
}
}
}
}
这里有个警告:不要在 “派生结果变化频率和原状态一致” 时使用 derivedStateOf ,例如,拼接用户名的时候 useName = firstName + SecondName,如果 firstName 变化派生结果(useName )必然变化,这时候使用 derivedStateOf 只会增加额外开销。
// 错误用法:没必要用derivedStateOf
var firstName by remember { mutableStateOf("") }
var secondName by remember { mutableStateOf("") }
val userName by remember { derivedStateOf { "$firstName $secondName" } }
// 正确用法:直接拼接即可
val userName = "$firstName $secondName"
三、重启效应
很多效应(如 LaunchedEffect、DisposableEffect、produceState)都支持 “键(key)” 参数,键的变化会触发 “重启效应”—— 即先取消当前效应,再启动新效应。但是用错可能会导致 bug 或性能问题,必须掌握以下规则:
1. 什么时候需要重启效应?
当效应的逻辑 “依赖某个值”,且 “该值变化时必须重新执行效应”—— 比如 Alpha 脉冲动画中脉冲的时间间隔 pulseRateMs 变化时,LaunchedEffect必须重启动画协程才能用新间隔。
当效应依赖的值变化,但不应该导致效应重启时,则应将该值用 rememberUpdatedState 包裹,效应中引用 State 对象的值,而非直接引用原变量(如 LandingScreen 中的 onTimeout 回调)。
2. 键的选择原则
- 效应代码块中使用的 “可变变量”(如
url、lifecycleOwner)必须作为键传入; - 效应代码块中使用的 “不可变变量”(如通过
remember记住且无键的对象)不需要作为键; - 可以用常量(如
true)作为键,让效应 “只启动一次,跟随可组合项生命周期”(谨慎使用)。
3. 常见错误案例
如果忘记将依赖的变量作为键,会导致效应不重启,使用旧值。例如,从网络加载图片的 loadNetworkImage 中的键 url:
// 错误:loadNetworkImage的键没有包含url,url变化时不会重新加载图片
produceState(initialValue = Result.Loading, key1 = imageRepository) {
val image = imageRepository.loadImage(url) // url变化但效应不重启,用旧url
// ...
}
// 正确:将url作为键,url变化时重启效应,加载新图片
produceState(initialValue = Result.Loading, key1 = url, key2 = imageRepository) {
// ...
}
四、总结
1. Side Effects - 附带效应
- 定义:发生在可组合函数作用域之外的应用状态变化,需通过效应 API 执行;
- 必要性:解决可组合项重组的不可预测性,避免重复执行、资源泄漏;
- 本质:效应是感知生命周期的可组合函数,不输出界面,只管理副作用。
2. 7 个效应 API 对比
| API | 适用场景 | 特点 |
|---|---|---|
LaunchedEffect | 可组合项内执行挂起函数 | 协程随组合生命周期,键变化重启 |
rememberCoroutineScope | 可组合项外启动协程,手动控制生命周期 | 作用域绑定组合,退出自动取消 |
rememberUpdatedState | 效应中引用值但不重启 | 持有最新值,避免效应频繁重启 |
DisposableEffect | 需要清理的操作(注册 / 取消监听) | 强制 onDispose 清理,键变化重启 |
SideEffect | Compose 状态同步到非 Compose 代码 | 每次重组成功后执行,确保同步及时 |
produceState | 非 Compose 状态转为 Compose State | 协程获取状态,自动转换为 State |
derivedStateOf | 降低派生状态的重组频率 | 只在结果变化时触发重组,优化性能 |
3. 重启效应关键规则
- 键需包含效应依赖的所有可变变量;
- 不想重启则用
rememberUpdatedState包裹变量; - 常量键(如
true)使效应跟随组合生命周期,谨慎使用。
884

被折叠的 条评论
为什么被折叠?



