在 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}")
// 显示购物车中的商品
}
}
常见误区与最佳实践
-
LaunchedEffect 的 key 不正确:
- 错误示例:未指定
key或使用错误的key,导致副作用频繁触发或根本不触发。 - 正确做法:确保
key参与到副作用的计算中,当其变化时,副作用的结果也随之更新。
- 错误示例:未指定
-
将 LaunchedEffect 与 MutableState 直接使用:
- 错误示例:直接将
MutableState对象作为LaunchedEffect的参数。 - 正确做法:使用状态的值(基本类型或不可变类型),而不是状态对象本身。
- 错误示例:直接将
-
未即时进行后处理:
- 错误示例:在不再需要副作用时,忘记处理清理操作(如网络请求、动画或监听器)。
- 正确做法:使用
DisposableEffect或手动清理资源,防止内存泄漏。
-
在 SideEffect 中调用挂起函数:
- 错误示例:在
SideEffect中调用挂起函数,导致编译错误。 - 正确做法:将耗时操作移至
LaunchedEffect或produceState。
- 错误示例:在