Compose 副作用
问题
Compose 中的副作用 API 有哪些?LaunchedEffect、DisposableEffect、SideEffect 分别在什么场景使用?
答案
1. 为什么需要副作用 API
Composable 函数应该是无副作用的(纯函数),但实际开发中不可避免需要:发起网络请求、注册监听器、写日志、操作外部系统等。Compose 提供了专用的 Effect API 来安全地执行这些操作。
2. 核心副作用 API
| API | 执行时机 | 协程 | 清理 | 典型场景 |
|---|---|---|---|---|
LaunchedEffect | 进入 Composition 时 | ✅ | 自动取消 | 网络请求、动画 |
DisposableEffect | 进入 Composition 时 | ❌ | onDispose | 注册/注销监听器 |
SideEffect | 每次重组成功后 | ❌ | ❌ | 同步 Compose 状态到外部 |
rememberCoroutineScope | 手动触发 | ✅ | Composable 离开时取消 | 点击事件触发的协程 |
produceState | 进入 Composition 时 | ✅ | 自动取消 | 将非 Compose 状态转为 State |
rememberUpdatedState | 始终引用最新值 | - | - | 长时间 Effect 中引用最新回调 |
3. LaunchedEffect
在 Composable 进入 Composition 时启动协程,key 变化时取消并重新启动:
@Composable
fun UserProfile(userId: String) {
var user by remember { mutableStateOf<User?>(null) }
// userId 变化时重新加载
LaunchedEffect(userId) {
user = repository.getUser(userId) // 挂起函数
}
user?.let { UserContent(it) }
}
// 一次性效果(key = Unit 或 true)
LaunchedEffect(Unit) {
// 只在初次进入 Composition 时执行一次
analytics.trackScreenView("home")
}
4. DisposableEffect
需要清理的副作用,离开 Composition 时执行 onDispose:
@Composable
fun LifecycleObserverEffect(onStart: () -> Unit, onStop: () -> Unit) {
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_START -> onStart()
Lifecycle.Event.ON_STOP -> onStop()
else -> {}
}
}
lifecycleOwner.lifecycle.addObserver(observer)
// 清理:移除监听器
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}
5. rememberCoroutineScope
用于在事件回调(如点击)中启动协程:
@Composable
fun SnackbarDemo(snackbarHostState: SnackbarHostState) {
val scope = rememberCoroutineScope()
Button(onClick = {
// 在点击事件中启动协程(不能用 LaunchedEffect)
scope.launch {
snackbarHostState.showSnackbar("操作成功")
}
}) {
Text("显示 Snackbar")
}
}
LaunchedEffect vs rememberCoroutineScope
LaunchedEffect:在 Composition 时自动启动,key 变化时重启rememberCoroutineScope:在用户交互(点击、手势)时手动启动
6. rememberUpdatedState
在长时间运行的 Effect 中始终引用最新的值:
@Composable
fun SplashScreen(onTimeout: () -> Unit) {
// 确保 Effect 中使用的是最新的 onTimeout 回调
val currentOnTimeout by rememberUpdatedState(onTimeout)
LaunchedEffect(Unit) {
delay(3000L)
currentOnTimeout() // 使用最新的回调
}
}
常见面试问题
Q1: LaunchedEffect 的 key 参数有什么作用?
答案:
key 用于控制 Effect 的重启时机:
- key 变化时:取消当前协程,重新启动新协程
- key 不变时:协程继续运行,不重启
LaunchedEffect(Unit):只在首次进入 Composition 时执行一次
// userId 变化 → 取消旧请求,发起新请求
LaunchedEffect(userId) {
val data = repository.fetch(userId)
}
Q2: SideEffect 和 LaunchedEffect 的区别?
答案:
SideEffect:每次重组成功后执行(同步),不启动协程。用于将 Compose 状态同步到非 Compose 代码LaunchedEffect:进入 Composition 时执行一次(或 key 变化时重启),启动协程
@Composable
fun AnalyticsTracker(screenName: String) {
// 每次 screenName 变化后同步到分析库
SideEffect {
analytics.setCurrentScreen(screenName)
}
}
Q3: produceState 是什么?
答案:
produceState 将非 Compose 的异步数据源转换为 Compose State:
@Composable
fun UserProfile(userId: String) {
val user by produceState<Result<User>>(initialValue = Result.Loading, userId) {
value = try {
Result.Success(repository.getUser(userId))
} catch (e: Exception) {
Result.Error(e)
}
}
// user 是 State<Result<User>>
}
本质上是 remember { mutableStateOf(initialValue) } + LaunchedEffect 的组合。
Q4: 如何在 Compose 中处理一次性事件(如 Toast、导航)?
答案:
推荐使用 Channel 或 SharedFlow + LaunchedEffect:
// ViewModel
class MyViewModel : ViewModel() {
private val _events = Channel<UiEvent>(Channel.BUFFERED)
val events = _events.receiveAsFlow()
fun onAction() {
viewModelScope.launch { _events.send(UiEvent.ShowToast("成功")) }
}
}
// Composable
@Composable
fun MyScreen(viewModel: MyViewModel) {
LaunchedEffect(Unit) {
viewModel.events.collect { event ->
when (event) {
is UiEvent.ShowToast -> { /* 显示 Toast */ }
is UiEvent.Navigate -> { /* 导航 */ }
}
}
}
}