9、状态管理(下):StateFlow与collectAsState、rememberSaveable、Compose中的副作用

好,我们接着聊状态管理。上一章我们把 Compose 里那些基础的状态工具捋了一遍,什么 mutableStateOfremember,还有状态提升。今天要聊的这几个,说实话,才是真正让你在项目里写出“能打”代码的关键。

我个人习惯把这一章的内容分成两块:一块是 跨页面/跨线程的状态流,另一块是 跟生命周期绑定的副作用。这两块搞明白了,你写 Compose 的时候就不会再问“为什么我的数据丢了?”或者“为什么我的协程还在跑?”这种问题了。

StateFlow 与 collectAsState

先说说 StateFlow。这东西说白了就是 Kotlin 协程家族里专门为“状态”设计的 Flow。它跟普通的 Flow 最大的区别是:它永远持有最新的值,而且当有新的订阅者时,它会立刻把当前值发出去。

我在项目中遇到过这样一个场景:用户登录后,我们需要在多个页面显示用户头像和昵称。如果用普通的 LiveData 或者 mutableStateOf,你得在各个页面手动同步。但用 StateFlow,我只需要在 ViewModel 里暴露一个 StateFlow<UserInfo>,所有页面通过 collectAsState 收集,数据自动同步。

核心要点:StateFlow 是热流,它不依赖收集者也能独立工作。而 collectAsState 是 Compose 专门为 StateFlow 提供的“桥接函数”,它会把 StateFlow 转换成 Compose 可观察的 State。

来看代码:

// ViewModel 中
class ProfileViewModel : ViewModel() {
    private val _userInfo = MutableStateFlow(UserInfo())
    val userInfo: StateFlow<UserInfo> = _userInfo.asStateFlow()

    fun loadUser() {
        viewModelScope.launch {
            val result = repository.fetchUser()
            _userInfo.value = result
        }
    }
}

// Composable 中
@Composable
fun ProfileScreen(viewModel: ProfileViewModel) {
    val userInfo by viewModel.userInfo.collectAsState()
    Text("用户名:${userInfo.name}")
}

嗯,这里要注意一点:collectAsState 默认会在 Composable 的生命周期内持续收集。如果你在后台页面,它也会继续收集,但不会触发重组。这个行为是 Compose 帮我们优化好的,你不需要额外操心。

我的小技巧:如果你在 ViewModel 里用 MutableStateFlow,记得用 .asStateFlow() 暴露出去。这样外部就只能读不能写,避免数据被意外篡改。我曾经在一个团队项目里没注意这个,结果某个同事直接在 Activity 里改了 StateFlow 的值,排查了半天……

rememberSaveable:让状态扛过配置变更

接下来聊 rememberSaveable。这个函数跟 remember 长得像,但功能强大多了。它不光能记住状态,还能在 Activity 重建(比如屏幕旋转、语言切换)后把状态恢复回来。

为什么会这样?因为 rememberSaveable 内部用了 SavedStateHandle,它会把数据序列化到 Bundle 里。所以,你存进去的东西必须能序列化。基本类型、String、甚至 Parcelable 都没问题,但如果你存了一个 Lambda 或者一个复杂的对象,那就得小心了。

我记得有一次,我在一个搜索框里用了 remember 来保存输入的关键词。结果用户旋转屏幕后,关键词丢了,用户气得差点摔手机。后来换成 rememberSaveable,问题就解决了。

@Composable
fun SearchBox() {
    var query by rememberSaveable { mutableStateOf("") }
    TextField(
        value = query,
        onValueChange = { query = it },
        placeholder = { Text("输入搜索关键词") }
    )
}

避坑指南:我曾经把一个自定义数据类直接塞进 rememberSaveable,结果崩溃了。后来才发现,自定义类必须实现 Parcelable 或者用 @Parcelize 注解。如果你不想写序列化代码,可以考虑用 mapSaver 或者 listSaver 来手动保存。

Compose 中的副作用:LaunchedEffect 与 DisposableEffect

副作用,说白了就是“跟 UI 无关但必须做的事”。比如发起网络请求、订阅传感器、启动定时器。Compose 里提供了几个专门的 Effect 函数来管理这些操作。

LaunchedEffect:在 Composable 中启动协程

LaunchedEffect 会在 Composable 进入组合时启动一个协程,并在离开组合时自动取消。它的 key 参数很关键:当 key 变化时,旧的协程会被取消,新的协程会启动。

我个人的习惯是:只要需要在 Composable 里做异步操作,第一反应就是用 LaunchedEffect。比如加载初始数据:

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

    LaunchedEffect(userId) {
        userData = repository.fetchUser(userId)
    }

    if (userData != null) {
        Text("用户:${userData.name}")
    } else {
        CircularProgressIndicator()
    }
}

你看,当 userId 变化时,旧的请求会被取消,新的请求自动发起。这比在 ViewModel 里手动管理协程要简洁得多。

注意:LaunchedEffect 的协程作用域是 rememberCoroutineScope(),但它会自动管理生命周期。如果你需要在 Composable 外部(比如点击事件)启动协程,那就用 rememberCoroutineScope() 手动创建作用域。

DisposableEffect:需要清理的副作用

有些副作用需要明确的清理操作,比如取消注册监听器、关闭数据库连接。这时候 DisposableEffect 就派上用场了。它会在 Composable 离开组合时执行清理代码。

我曾经在做一个传感器应用时,需要在 Composable 里注册一个加速度传感器监听器。如果忘记取消注册,Activity 销毁后传感器还在跑,不仅耗电,还会导致内存泄漏。用 DisposableEffect 就能完美解决:

@Composable
fun SensorDisplay(sensorManager: SensorManager) {
    DisposableEffect(sensorManager) {
        val sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
        val listener = object : SensorEventListener {
            override fun onSensorChanged(event: SensorEvent?) {
                // 处理传感器数据
            }
            override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
        }
        sensorManager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_NORMAL)

        onDispose {
            sensorManager.unregisterListener(listener)
        }
    }
}

嗯,这里的关键是 onDispose 块。它里面的代码一定会在 Composable 从组合中移除时执行,不管是因为导航离开、屏幕旋转还是其他原因。

知识体系总览

为了让你更直观地理解这一章的内容,我画了一张图。它把状态管理和副作用的核心逻辑串起来了:

Compose 状态管理 & 副作用核心结构 状态管理 StateFlow rememberSaveable collectAsState SavedStateHandle 副作用 LaunchedEffect DisposableEffect 协程 + 自动取消 onDispose 清理 核心原则:状态与生命周期绑定,避免内存泄漏与数据丢失 相辅相成 StateFlow 负责跨组件状态同步,rememberSaveable 负责配置变更恢复 LaunchedEffect 管理异步任务,DisposableEffect 管理资源清理

总结一下

这一章的内容其实就三个核心点:

  • StateFlow + collectAsState:解决跨页面、跨线程的状态同步问题。记住用 .asStateFlow() 保护数据。
  • rememberSaveable:让你的状态在屏幕旋转等配置变更后依然存活。注意序列化问题。
  • LaunchedEffect 和 DisposableEffect:把副作用交给 Compose 管理,别自己瞎搞协程生命周期。

说实话,这些工具刚接触时可能会觉得有点绕,但用多了你会发现,它们其实是在帮你减少犯错的机会。你想想看,如果没有 DisposableEffect,你每次都得在 onStoponDestroy 里手动清理,多容易漏啊。

好了,这一章就到这里。代码不多,但每个都是实战中高频使用的。你可以在自己的项目里试着把 remember 换成 rememberSaveable,把 LiveData 换成 StateFlow,感受一下区别。嗯,相信我,你会爱上这种“再也不用担心状态丢失”的感觉的。

公众号:蓝海资料掘金营,微信deep3321