compose 附带效应 笔记二

3 阅读3分钟

在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)
    }
}

🎯 效应处理的最高原则

  1. 效应应该幂等:效应的启动应依赖于明确的keyLaunchedEffect(key1)),确保效应只在需要时重启,避免无限循环。
  2. 效应应在正确生命周期启动:使用 rememberCoroutineScope 在响应UI事件(如点击)时启动协程,而非在组合中直接启动。
    @Composable
    fun MyButton() {
        val scope = rememberCoroutineScope()
        Button(onClick = {
            scope.launch { // 响应UI事件
                // 执行挂起操作
            }
        }) {
            Text("Click Me")
        }
    }
    

💎 总结:使用效应API的心智模型

  • 问自己:“这个操作是响应UI事件(如点击),还是响应状态/组合变化?”
    • 响应UI事件 → 使用 rememberCoroutineScope().launch
    • 响应状态变化 → 使用对应的效应API(LaunchedEffectDisposableEffect 等)。
  • 再问:“这个操作需要清理吗?”
    • 需要 → 使用 DisposableEffect
    • 不需要 → 使用 LaunchedEffectSideEffect

效应是Compose中连接声明式UI与命令式世界的安全通道。正确使用它们,是构建稳定、高效Compose应用的关键。