在Compose中,附带效应(Side-Effect)指的是在可组合函数作用域之外发生的操作。由于可组合函数可能会在任何时间因重组被频繁调用,任何可能影响应用状态、资源或外部系统的操作(如发起网络请求、更新数据库、启动动画等)都必须通过专门的效应API来管理。
简单说:效应是连接声明式UI(Compose)与外部命令式世界的安全桥梁。
🔧 为什么需要效应API?
如果直接在可组合函数中执行副作用,会导致严重问题:
@Composable
fun DangerousScreen() {
// ❌ 危险操作:每次重组都会执行!
Launcher.launch {
requestData() // 网络请求
}
}
上面的代码在每次UI重组时都会启动新的协程,造成请求重复发送、资源泄露。
📚 核心效应API及其适用场景
Compose提供了一系列效应API,用于在不同的生命周期时机安全地执行副作用。
| API | 核心用途 | 触发时机 | 适用场景 |
|---|---|---|---|
LaunchedEffect | 在可组合项中安全启动协程 | 首次组合,或key变化时 | 执行挂起操作:一次性动画、ViewModel的挂起函数调用。 |
DisposableEffect | 需要清理的资源 | 首次组合,或key变化时;组合退出或key变化时执行清理 | 监听器注册/注销、订阅生命周期回调。 |
SideEffect | 将Compose状态同步给非Compose代码 | 每次成功的重组后 | 将状态(如用户权限)同步给Firebase Analytics等统计SDK。 |
produceState | 将非Compose异步流转换为Compose State | 首次组合启动协程,组合退出取消 | 简单地将外部数据流(如网络、数据库)桥接为状态。 |
derivedStateOf | 将一个或多个状态转换为派生状态 | 其依赖的状态变化时 | 计算复杂或开销大的派生值,如列表过滤、表单校验。 |
🚀 关键API详解与代码示例
1. LaunchedEffect:执行挂起操作
它在组合中启动一个协程,该协程会在组合退出或key变化时自动取消。
@Composable
fun MyScreen(viewModel: MyViewModel) {
val data by viewModel.uiState.collectAsState()
// 当viewModel变化时,启动新的协程,旧的会被取消
LaunchedEffect(key1 = viewModel) {
viewModel.loadInitialData() // 安全的挂起函数调用
}
// 根据状态显示UI
when (data) {
is Loading -> CircularProgressIndicator()
is Success -> DataList(data)
}
}
2. DisposableEffect:管理需要清理的资源
用于注册和清理操作,确保没有资源泄漏。
@Composable
fun LocationTracker(locationCallback: (Location) -> Unit) {
val context = LocalContext.current
val locationManager = remember { context.getSystemService(LOCATION_SERVICE) as LocationManager }
DisposableEffect(key1 = locationManager) {
// 注册监听(效应)
val listener = LocationListener { locationCallback(it) }
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0L, 0f, listener)
// 清理函数(组合退出或key变化时执行)
onDispose {
locationManager.removeUpdates(listener)
}
}
}
3. SideEffect:同步状态到非Compose世界
用于在每次成功的重组后,将Compose状态发布给共享对象。
@Composable
fun AnalyticsTracker(userId: String?) {
val firebaseAnalytics = remember { Firebase.analytics }
// 每次重组后,如果userId变化,就同步给Firebase
SideEffect {
firebaseAnalytics.setUserId(userId)
}
}
🎯 效应处理的最高原则
- 效应应该幂等:效应的启动应依赖于明确的key(
LaunchedEffect(key1)),确保效应只在需要时重启,避免无限循环。 - 效应应在正确生命周期启动:使用
rememberCoroutineScope在响应UI事件(如点击)时启动协程,而非在组合中直接启动。@Composable fun MyButton() { val scope = rememberCoroutineScope() Button(onClick = { scope.launch { // 响应UI事件 // 执行挂起操作 } }) { Text("Click Me") } }
💎 总结:使用效应API的心智模型
- 问自己:“这个操作是响应UI事件(如点击),还是响应状态/组合变化?”
- 响应UI事件 → 使用
rememberCoroutineScope().launch。 - 响应状态变化 → 使用对应的效应API(
LaunchedEffect,DisposableEffect等)。
- 响应UI事件 → 使用
- 再问:“这个操作需要清理吗?”
- 需要 → 使用
DisposableEffect。 - 不需要 → 使用
LaunchedEffect或SideEffect。
- 需要 → 使用
效应是Compose中连接声明式UI与命令式世界的安全通道。正确使用它们,是构建稳定、高效Compose应用的关键。