副作用 Side Effects

41 阅读2分钟

在 Compose UI 中,副作用(Side Effects)是指发生在可组合函数作用域之外的应用状态的变化,用于处理与 UI 渲染无关的操作,例如数据加载、动画控制、事件监听等。这些操作通常需要在特定时机执行,并且需要与 Compose 的生命周期协同工作。以下是 Compose UI 中常见的副作用 API 的使用说明及示例:

1. LaunchedEffect

用途:在可组合函数中启动协程,用于执行异步任务(如网络请求、数据加载等)。协程的生命周期与可组合函数绑定,当可组合函数离开组合时,协程会自动取消。

关键点

  • 通过 key 参数控制协程的重启行为。当 key 变化时,协程会重新启动;当 key 不变时,协程不会重新执行。
  • 如果希望协程仅运行一次,可以使用 LaunchedEffect(Unit)

示例

@Composable
fun UserProfile(userId: String) {
    var userData by remember { mutableStateOf<User?>(null) }

    // 当 userId 变化时,重新加载用户数据
    LaunchedEffect(userId) {
        userData = fetchUserFromNetwork(userId) // 假设这是一个挂起函数
    }

    // 显示用户数据或加载状态
    when {
        userData != null -> Text("Hello, ${userData!!.name}")
        else -> CircularProgressIndicator()
    }
}

2. DisposableEffect

用途:在可组合函数中管理需要清理的资源(如事件监听器、动画、订阅等)。当可组合函数离开组合或 key 变化时,会自动调用清理逻辑。

关键点

  • 必须提供 onDispose 块,用于释放资源。
  • key 变化时,会先执行上一次的 onDispose,然后重新执行副作用代码。

示例

@Composable
fun BatteryLevelMonitor(lifecycleOwner: LifecycleOwner) {
    DisposableEffect(lifecycleOwner) {
        val observer = LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_LOW_MEMORY) {
                // 处理低内存事件
            }
        }
        lifecycleOwner.lifecycle.addObserver(observer)

        // 清理逻辑
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }
}

3. SideEffect

用途:在每次重组(Recomposition)时执行副作用操作。适用于与 UI 状态无关的轻量级操作(如日志记录、埋点等)。

关键点

  • 没有 key 参数,每次重组都会执行。
  • 不能调用挂起函数。

示例

@Composable
fun TrackScreenView(screenName: String) {
    SideEffect {
        // 记录屏幕浏览事件
        analyticsTracker.trackScreenView(screenName)
    }

    Text("Screen: $screenName")
}

4. rememberCoroutineScope

用途:获取与当前组合作用域绑定的 CoroutineScope,用于在非挂起函数(如按钮点击回调)中启动协程。

关键点

  • 启动的协程会在可组合函数离开组合时自动取消。
  • 适用于需要响应 UI 事件(如点击)而触发的异步操作。

示例

@Composable
fun SubmitButton() {
    val coroutineScope = rememberCoroutineScope()

    Button(onClick = {
        coroutineScope.launch {
            // 模拟耗时操作
            delay(1000)
            // 处理提交逻辑
        }
    }) {
        Text("Submit")
    }
}

5. produceState

用途:将非 Compose 的异步数据源(如网络请求、数据库查询、Flow)转换为 Compose 的 State

关键点

  • 在后台使用 LaunchedEffect 管理异步操作。
  • 自动管理状态和重组。

示例

@Composable
fun UserProfile(userId: String): State<User?> {
    return produceState(initialValue = null, userId) {
        // 在协程中加载数据
        val userData = fetchUserFromNetwork(userId) // 假设这是一个挂起函数
        value = userData
    }
}

@Composable
fun UserScreen(userId: String) {
    val userData = UserProfile(userId).value

    when {
        userData != null -> Text("Hello, ${userData.name}")
        else -> CircularProgressIndicator()
    }
}

6. derivedStateOf

用途:从一个或多个其他状态中派生出一个新的状态。优化性能,只有当依赖的状态发生变化且计算结果改变时,才会触发重组。

示例

@Composable
fun ShoppingCart() {
    val items = remember { mutableStateListOf<Item>() }
    val totalPrice = derivedStateOf {
        items.sumOf { it.price }
    }

    Column {
        Text("Total Price: $${totalPrice.value}")
        // 显示购物车中的商品
    }
}

常见误区与最佳实践

  1. LaunchedEffect 的 key 不正确

    • 错误示例:未指定 key 或使用错误的 key,导致副作用频繁触发或根本不触发。
    • 正确做法:确保 key 参与到副作用的计算中,当其变化时,副作用的结果也随之更新。
  2. 将 LaunchedEffect 与 MutableState 直接使用

    • 错误示例:直接将 MutableState 对象作为 LaunchedEffect 的参数。
    • 正确做法:使用状态的值(基本类型或不可变类型),而不是状态对象本身。
  3. 未即时进行后处理

    • 错误示例:在不再需要副作用时,忘记处理清理操作(如网络请求、动画或监听器)。
    • 正确做法:使用 DisposableEffect 或手动清理资源,防止内存泄漏。
  4. 在 SideEffect 中调用挂起函数

    • 错误示例:在 SideEffect 中调用挂起函数,导致编译错误。
    • 正确做法:将耗时操作移至 LaunchedEffectproduceState