跳到主要内容

Compose 副作用

问题

Compose 中的副作用 API 有哪些?LaunchedEffectDisposableEffectSideEffect 分别在什么场景使用?

答案

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: SideEffectLaunchedEffect 的区别?

答案

  • 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、导航)?

答案

推荐使用 ChannelSharedFlow + 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 -> { /* 导航 */ }
}
}
}
}

相关链接