首页 / Android 入门教程 / Compose 副作用

Android 入门教程

Compose 副作用

本教程共 100 篇 · 第 54 篇 · 更新于 2026-07-28 · 约 9 分钟阅读

AndroidAndroid 入门教程Jetpack Compose副作用LaunchedEffectDisposableEffectproduceState

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 的互操作。