Compose 副作用
本教程共 100 篇 · 第 54 篇 · 更新于 2026-07-28 · 约 9 分钟阅读
54. Compose 副作用
本节目标:理解 Compose 中副作用的概念,学会用 LaunchedEffect、DisposableEffect、produceState 等 API 安全地执行副作用,避免重组导致的 bug。
什么是副作用
第 42 章说过,可组合函数应该无副作用。但实际开发中,你总得做点「影响外部世界」的事:
- 发起网络请求
- 读写数据库
- 启动协程
- 注册/注销监听器
这些操作不能随便放在函数体里,因为重组时机和次数不可控。Compose 提供了一组专门的副作用 API,让这些操作在安全可控的时机执行。
LaunchedEffect 启动协程
LaunchedEffect 在可组合函数进入组合时启动协程,离开时自动取消。适合做一次性操作:
@Composable
fun DataLoader(userId: String) {
var data by remember { mutableStateOf<String?>(null) }
// userId 变化时重新加载
LaunchedEffect(userId) {
data = fetchData(userId) // suspend 函数
}
if (data == null) {
LoadingIndicator()
} else {
Text(data!!)
}
}
关键点:
LaunchedEffect(userId)的参数是 key。key 变化时,旧协程取消,新协程启动。- 传入
Unit表示只在首次进入组合时执行一次。 - 传入多个 key,任一变化都重启。
Warning
- 别忘了传 key。如果你用
LaunchedEffect(Unit)加载数据,但数据依赖的参数变了,不会重新加载。正确做法是把依赖参数作为 key。
在 LaunchedEffect 里做延迟
@Composable
fun DelayedMessage() {
var show by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
delay(2000) // 2 秒后显示
show = true
}
if (show) Text("我出现了")
}
rememberCoroutineScope
LaunchedEffect 是声明式的,但有时候你需要在事件回调里启动协程(比如按钮点击后请求网络)。用 rememberCoroutineScope:
@Composable
fun SaveButton() {
val scope = rememberCoroutineScope()
var saving by remember { mutableStateOf(false) }
Button(
onClick = {
saving = true
scope.launch {
saveData() // suspend
saving = false
}
},
enabled = !saving
) {
if (saving) {
CircularProgressIndicator(modifier = Modifier.size(16.dp))
} else {
Text("保存")
}
}
}
Note
rememberCoroutineScope返回的协程作用域绑定到组合的生命周期。组件离开组合时,作用域内所有协程自动取消。别用CoroutineScope()手动创建,那样不受 Compose 管理,容易泄漏。
DisposableEffect 清理资源
需要在组件离开组合时清理资源(注销监听器、关闭连接)用 DisposableEffect:
@Composable
fun LifecycleAwareComponent() {
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> Log.d("Lifecycle", "恢复")
Lifecycle.Event.ON_PAUSE -> Log.d("Lifecycle", "暂停")
else -> {}
}
}
lifecycleOwner.lifecycle.addObserver(observer)
// 清理逻辑放在 onDispose 里
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}
Tip
DisposableEffect的 key 变化时也会先执行onDispose清理旧的,再重新执行。适合注册/注销成对出现的操作。
produceState 把非 Compose 数据转成状态
把协程结果、Flow、回调等「外部数据源」转成 Compose 可观察的状态:
@Composable
fun UserAvatar(userId: String) {
// 把网络请求结果转成 State
val imageState by produceState(initialValue = null as ImageBitmap?, userId) {
// 这里是协程作用域,value = xxx 可以设值
value = loadImage(userId)
}
if (imageState != null) {
Image(imageState!!, contentDescription = null)
} else {
LoadingIndicator()
}
}
把 Flow 转成 State:
@Composable
fun FlowCollector(flow: Flow<String>) {
val text by produceState(initialValue = "", flow) {
flow.collect { value = it }
}
Text(text)
}
Note
produceState返回State<T>,用by委托读取。初始值在数据到达前显示。当 key 变化时自动重新生产。
derivedStateOf 派生状态
当一个状态是其他状态的「计算结果」时用 derivedStateOf,避免不必要的重组:
@Composable
fun TodoList(items: List<TodoItem>) {
val doneCount by remember {
derivedStateOf { items.count { it.isDone } }
}
Text("已完成 $doneCount / ${items.size}")
}
// 滚动到顶才显示按钮
@Composable
fun ScrollToTopButton(scrollState: ScrollState) {
val showButton by remember {
derivedStateOf { scrollState.value > 500 }
}
if (showButton) {
FloatingActionButton(onClick = { /* 回顶 */ }) {
Icon(Icons.Default.ArrowUpward, contentDescription = null)
}
}
}
Warning
derivedStateOf适合「频繁变化的状态派生出布尔值/少量值」的场景。如果不加它,scrollState.value > 500每帧滚动都触发重组;加了之后只有true/false切换时才重组。但别滥用,简单的计算直接用普通变量即可。
SideEffect 每次重组后执行
把 Compose 状态同步到非 Compose 对象(比如状态栏颜色):
@Composable
fun SyncSystemUi(darkTheme: Boolean) {
val systemUiController = rememberSystemUiController()
SideEffect {
systemUiController.setSystemBarsColor(
color = if (darkTheme) Color.Black else Color.White,
darkIcons = !darkTheme
)
}
}
Note
SideEffect在每次成功重组后执行。适合做「Compose -> 外部世界」的单向同步。注意它不保证只执行一次。
rememberUpdatedState 捕获最新值
在长生命周期的副作用里,需要引用可能变化的值,但不想重启副作用:
@Composable
fun Timer(onTimeout: () -> Unit) {
// onTimeout 可能每次重组都变,但 LaunchedEffect 不该重启
val currentOnTimeout by rememberUpdatedState(onTimeout)
LaunchedEffect(Unit) {
delay(5000)
currentOnTimeout() // 调用最新的回调
}
}
Tip
- 如果
LaunchedEffect的 key 是Unit(只执行一次),但里面用到的回调会变,用rememberUpdatedState包装它。这样回调变了副作用不重启,但调用时用的是最新值。
副作用 API 速查
| API | 触发时机 | 适合场景 |
|---|---|---|
LaunchedEffect | 进入组合 / key 变化 | 一次性 suspend 操作 |
rememberCoroutineScope | 手动触发 | 事件回调中启动协程 |
DisposableEffect | 进入组合 / key 变化 + 离开 | 注册/注销资源 |
produceState | 进入组合 / key 变化 | 外部数据转 State |
derivedStateOf | 依赖状态变化 | 派生计算结果 |
SideEffect | 每次重组后 | 同步到外部对象 |
rememberUpdatedState | 每次重组 | 捕获最新值 |
小结
副作用是 Compose 中和外部世界交互的桥梁。LaunchedEffect 做一次性协程,rememberCoroutineScope 在回调里启动协程,DisposableEffect 管理资源注册注销,produceState 转换外部数据,derivedStateOf 优化派生状态。用对了能让代码既安全又高效。下一节学习 Compose 和传统 View 的互操作。