Compose 中的 Side-effects

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 数据加载与网络请求

使用 LaunchedEffectproduceState 在 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 或仓库层,便于单独测试
  • 使用 runTestTestDispatcher 测试协程副作用
  • 考虑使用依赖注入提供测试替身(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 优化计算:对复杂计算结果进行缓存
  • 限制重组范围 :使用 rememberLaunchedEffect 的 key 参数控制重组
kotlin 复制代码
// 优化前:每次滚动都会重新计算
val visibleItems = items.filter { it.isVisible }

// 优化后:只有 items 变化时才重新计算
val visibleItems by remember(items) {
    derivedStateOf { items.filter { it.isVisible } }
}

注意:过度使用副作用或不正确地管理副作用可能导致应用不稳定、性能问题或难以调试的错误。始终遵循 Compose 的单向数据流原则,将业务逻辑与 UI 分离。

相关推荐
安东尼肉店1 小时前
Android compose屏幕适配终极解决方案
android
2501_916007471 小时前
HTTPS 抓包乱码怎么办?原因剖析、排查步骤与实战工具对策(HTTPS 抓包乱码、gzipbrotli、TLS 解密、iOS 抓包)
android·ios·小程序·https·uni-app·iphone·webview
feiyangqingyun3 小时前
基于Qt和FFmpeg的安卓监控模拟器/手机摄像头模拟成onvif和28181设备
android·qt·ffmpeg
用户2018792831677 小时前
ANR之RenderThread不可中断睡眠state=D
android
煤球王子7 小时前
简单学:Android14中的Bluetooth—PBAP下载
android
小趴菜82277 小时前
安卓接入Max广告源
android
齊家治國平天下7 小时前
Android 14 系统 ANR (Application Not Responding) 深度分析与解决指南
android·anr
ZHANG13HAO7 小时前
Android 13.0 Framework 实现应用通知使用权默认开启的技术指南
android
【ql君】qlexcel7 小时前
Android 安卓RIL介绍
android·安卓·ril
写点啥呢7 小时前
android12解决非CarProperty接口深色模式设置后开机无法保持
android·车机·aosp·深色模式·座舱