1. 什么是副作用
副作用 是指应用程式状态在可组合函式的范围以外发生变化。由于可组合项的生命周期和属性可能有例如预期之外的重新组成、以不同顺序重新组成可组合项,或可舍弃的重新组成等等因素,因此可组合项最好应该没有任何副作用。
但是有时候我们又必须用到副作用,例如想在特定状态条件下触发显示Snackbar 或导览到其他萤幕等单次事件时就必须利用副作用。您应透过会考量可组合项生命周期的受监控环境呼叫这些操作。在这个网页中,我们会说明Jetpack Compose 提供的各种副作用API。
2. 副作用 API
2.1 LaunchedEffect
在 Composable 作用域内启动协程的最基本方式,会在 Composable 进入组合时启动,并在退出组合或依赖项变化时取消并重启。
kotlin
LaunchedEffect(key1 = userId) {
// 在协程中执行副作用操作
val userData = userRepository.fetchUser(userId)
// 更新状态
_userState.value = userData
}
关键点:
- 参数
key
,用于确定何时需要重启LaunchedEffect - 内部代码运行在协程中,可以安全调用挂起函数
- 自动管理协程生命周期,与 Composable 生命周期绑定,Composable 退出组合时自动取消内部协程
2.2 rememberCoroutineScope
获取与 Composable 生命周期绑定的协程作用域,用于在 Composable 函数之外(如按钮点击事件)启动协程。
kotlin
val scope = rememberCoroutineScope()
Button(onClick = {
scope.launch {
// 在按钮点击时执行协程操作
submitData()
}
}) {
Text("提交")
}
关键点:
- 协程作用域与 Composable 生命周期绑定,Composable 退出组合时自动取消
2.3 DisposableEffect
用于需要清理的副作用,确保资源在 Composable 退出组合或依赖变化时得到正确释放。
kotlin
DisposableEffect(key1 = lifecycleOwner) {
// 注册监听器
val observer = LifecycleEventObserver { _, event ->
// 处理生命周期事件
}
lifecycleOwner.lifecycle.addObserver(observer)
// 返回清理函数
onDispose {
// 移除监听器,防止内存泄漏
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
关键点:
- 必须 返回一个
onDispose
函数来清理资源,当Composable 退出组合的时候执行 - 适用于注册/注销监听器、订阅/取消订阅、释放资源等场景
2.4 SideEffect
在每次 Composable 成功重组后执行的副作用,没有键参数,每次重组都会执行。
kotlin
SideEffect {
// 记录重组次数或更新分析数据
analyticsTracker.trackScreenView("HomeScreen")
}
关键点:
- 无键参数,每次重组都会执行
- 同步执行,不能调用挂起函数
- 通常用于轻量级操作,如记录日志或更新分析数据
2.5 produceState
将非 Compose 状态转换为 Compose 状态,在后台协程中计算并产生状态。
kotlin
val userData by produceState<UserData?>(
initialValue = null,
key = userId
) {
// 后台加载数据
value = userRepository.fetchUser(userId)
}
关键点:
- 将非状态数据转换为 State 对象
- 自动管理加载状态
- 适用于从数据层获取数据并转换为 UI 状态
2.6 derivedStateOf
从其他状态派生出新状态,只有当依赖状态变化且计算结果不同时才会触发重组。
kotlin
val searchResults by remember {
derivedStateOf {
// 只有当查询或列表变化时才重新计算
items.filter { it.matches(searchQuery.value) }
}
}
关键点:
- 用于优化性能,减少不必要的重组
- 不是严格意义上的副作用 API,但用于状态转换和优化
- 计算应快速完成,不应包含副作用
注意点
依赖收集机制:
derivedStateOf
会在首次计算时自动追踪所有在计算过程中读取的State
对象,并将它们作为依赖项。- 如果某个
State
只在条件分支中被读取,而首次计算时未进入该分支,则该State
不会被注册为依赖项。
kotlin
val derivedValue = derivedStateOf {
if (conditionState.value) { // 如果首次计算时 conditionState.value == false
stateA.value // 则 stateA 不会被注册为依赖
} else {
stateB.value // 只有 stateB 成为依赖
}
}
3. 副作用管理原则
3.1 作用域与生命周期
- 遵循最小作用域原则:副作用应在尽可能小的作用域内执行
- 生命周期感知:确保副作用与 Composable 生命周期正确绑定,避免内存泄漏
- 及时清理:所有注册操作都应有对应的注销操作
3.2 依赖参数
- 谨慎选择 key:副作用 API 的 key 参数决定何时重新执行副作用
- 最小化依赖集:只包含影响副作用结果的必要依赖项
- 避免不稳定依赖:不要使用频繁变化的对象作为 key
3.3 避免不必要的副作用
- 状态提升:将共享状态提升到更高层级管理
- 纯 UI 逻辑:尽量将逻辑移至 Composable 之外或使用 ViewModel
- 防抖处理:对频繁变化的状态使用防抖或节流
4. 常见副作用场景
4.1 数据加载与网络请求
使用 LaunchedEffect
或 produceState
在 Composable 进入组合时加载数据:
kotlin
val uiState by produceState<UiState>(initialValue = UiState.Loading) {
try {
val data = repository.getData()
value = UiState.Success(data)
} catch (e: Exception) {
value = UiState.Error(e.message)
}
}
4.2 订阅与监听
使用 DisposableEffect
处理需要注册和注销的监听器:
kotlin
DisposableEffect(key1 = connectivityManager) {
val networkCallback = object : ConnectivityManager.NetworkCallback() {
// 网络状态变化处理
}
connectivityManager.registerDefaultNetworkCallback(networkCallback)
onDispose {
connectivityManager.unregisterNetworkCallback(networkCallback)
}
}
4.3 UI 状态更新
响应状态变化并更新 UI 相关设置:
kotlin
LaunchedEffect(key1 = isDarkMode) {
// 根据主题模式更新系统状态栏
systemUiController.setSystemBarsColor(
color = if (isDarkMode) Color.Black else Color.White
)
}
4.4 日志与分析
使用 SideEffect
记录用户交互和界面展示:
kotlin
SideEffect {
analytics.logScreenView(screenName = "ProductDetail")
}
Button(onClick = {
scope.launch {
trackButtonClick("addToCart")
addToCart(productId)
}
}) {
Text("加入购物车")
}
5. 最佳实践与注意事项
5.1 单一职责原则
每个副作用函数应只负责一项任务,保持副作用代码简洁明确。
kotlin
// 不推荐:一个副作用做太多事情
LaunchedEffect(Unit) {
loadUserData()
trackScreenView()
setupAnalytics()
}
// 推荐:拆分多个副作用
LaunchedEffect(userId) { loadUserData() }
SideEffect { trackScreenView() }
DisposableEffect(Unit) {
setupAnalytics()
onDispose { cleanupAnalytics() }
}
5.2 测试副作用
- 将复杂副作用逻辑移至 ViewModel 或仓库层,便于单独测试
- 使用
runTest
和TestDispatcher
测试协程副作用 - 考虑使用依赖注入提供测试替身(Fake)
kotlin
@RunWith(JUnit4::class)
class UserScreenTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun loadUserData_displaysUserInfo() = runTest {
// 使用测试调度器和假仓库
val fakeRepository = FakeUserRepository()
composeTestRule.setContent {
UserScreen(
userId = "123",
repository = fakeRepository,
dispatcher = testDispatcher
)
}
// 验证UI状态
composeTestRule.onNodeWithText("John Doe").assertIsDisplayed()
}
}
5.3 性能考量
- 避免过度使用副作用:不必要的副作用会导致性能问题
- 合理设置 key 参数:避免因错误的 key 导致副作用频繁重启
- 使用 derivedStateOf 优化计算:对复杂计算结果进行缓存
- 限制重组范围 :使用
remember
和LaunchedEffect
的 key 参数控制重组
kotlin
// 优化前:每次滚动都会重新计算
val visibleItems = items.filter { it.isVisible }
// 优化后:只有 items 变化时才重新计算
val visibleItems by remember(items) {
derivedStateOf { items.filter { it.isVisible } }
}
注意:过度使用副作用或不正确地管理副作用可能导致应用不稳定、性能问题或难以调试的错误。始终遵循 Compose 的单向数据流原则,将业务逻辑与 UI 分离。