12、Compose状态管理:remember与mutableStateOf、StateFlow与Compose、Compose副作用

状态管理,说白了就是让UI能跟着数据变。在Compose里,这件事做得特别优雅,但也特别容易踩坑。我刚开始从View系统切到Compose时,最不适应的就是「状态到底该放哪」这个问题。今天咱们就把这块彻底捋清楚。

12.1 remember与mutableStateOf:最基础的状态容器

先看一个最简单的例子。你想让一个按钮被点击后,文字从「点我」变成「已点击」。

@Composable
fun ClickCounter() {
    var count by remember { mutableStateOf(0) }

    Button(onClick = { count++ }) {
        Text("点击次数: $count")
    }
}

这里有两个关键点:mutableStateOf 创建了一个可观察的状态对象,remember 保证它在重组时不被重置。嗯,这里要注意——remember 只在当前 Composable 的生命周期内有效。如果屏幕旋转或者配置变更,它还是会丢的。

核心理解:

  • mutableStateOf 是数据的「可观察包装」
  • remember 是「跨重组保持」的缓存机制
  • 两者结合,就是Compose中最基础的状态管理单元

我在项目中遇到过一个问题:有个列表的选中状态,我用 remember 存了,结果用户切到后台再回来,选中状态全丢了。为什么?因为 remember 不处理进程重建。这时候就得用 rememberSaveable

var selectedId by rememberSaveable { mutableStateOf(-1) }

rememberSaveable 会把状态存到 Bundle 里,能扛住配置变更和进程重建。但注意,它只能存基本类型和可序列化的对象。复杂对象你得自己写 Saver

12.2 StateFlow与Compose:响应式数据流

实际项目中,状态不可能全写在 Composable 里。ViewModel 里管理数据,Compose 里消费数据——这才是正经架构。

我个人习惯用 StateFlow 来桥接 ViewModel 和 Compose。为什么不用 LiveData?因为 StateFlow 是 Kotlin 协程原生支持的,配合 Compose 的 collectAsState() 用起来特别顺。

class MyViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(UiState())
    val uiState: StateFlow<UiState> = _uiState.asStateFlow()

    fun updateName(name: String) {
        _uiState.update { it.copy(name = name) }
    }
}

data class UiState(
    val name: String = "",
    val isLoading: Boolean = false
)

@Composable
fun MyScreen(viewModel: MyViewModel) {
    val uiState by viewModel.uiState.collectAsState()

    Text("当前名字: ${uiState.name}")
}

你想想看,collectAsState() 做了什么?它把 StateFlow 的发射值转成了 Compose 的 State。每次 StateFlow 发新值,Compose 就会自动重组。这就是响应式UI的核心。

个人经验:

我建议在 ViewModel 里用 MutableStateFlow + update() 来更新状态。update() 是原子操作,能避免并发问题。别用 value = xxx 直接赋值,容易出竞态。

还有一个坑:StateFlow 默认是「粘性的」。就是说,如果你在 Composable 里 collect 一个 StateFlow,它会立刻拿到最近一次的值。这通常没问题,但如果你用 SharedFlow 做一次性事件(比如 Snackbar 提示),就得小心了。

// 一次性事件,用 SharedFlow
val events: SharedFlow<UiEvent> = _events.asSharedFlow()

12.3 Compose副作用:LaunchedEffect、DisposableEffect、SideEffect

副作用,就是「在 Composable 之外做的事情」。比如网络请求、数据库操作、注册监听器。Compose 提供了几个专门的副作用函数,用错了会出大问题。

12.3.1 LaunchedEffect:异步任务

想在 Composable 里启动协程?用 LaunchedEffect

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

    LaunchedEffect(userId) {
        user = api.fetchUser(userId)
    }

    Text(user?.name ?: "加载中...")
}

LaunchedEffect 的 key 参数很关键。key 变了,它会取消旧协程,启动新协程。key 不变,它只执行一次。我曾经犯过一个错:把 key 写成了常量,结果 userId 变了也不重新请求——页面一直显示旧数据。

避坑指南:

我曾经在 LaunchedEffect 里直接调用了 ViewModel 的方法,结果 Composable 重组时反复触发。正确的做法是:ViewModel 的方法应该幂等,或者用 remember 做防抖。

12.3.2 DisposableEffect:清理资源

需要注册监听器、传感器、或者 Lifecycle 回调?用 DisposableEffect。它会在离开 Composable 时自动清理。

@Composable
fun LifecycleObserverEffect(onResume: () -> Unit) {
    DisposableEffect(Unit) {
        val observer = LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_RESUME) {
                onResume()
            }
        }
        val lifecycle = LocalLifecycleOwner.current.lifecycle
        lifecycle.addObserver(observer)

        onDispose {
            lifecycle.removeObserver(observer)
        }
    }
}

注意 onDispose 里的清理逻辑。不清理的话,Composable 销毁了监听器还在,轻则内存泄漏,重则崩溃。

12.3.3 SideEffect:同步副作用

SideEffect 在每次重组成功后会执行。我很少用它,除非需要把 Compose 的状态同步到非 Compose 的世界里。

@Composable
fun AnalyticsScreen(screenName: String) {
    SideEffect {
        Analytics.trackScreen(screenName)
    }
}

但注意,SideEffect 不能做异步操作。它就是个同步回调。

12.4 知识体系总览

下面这张图,把今天讲的核心概念串起来了。你仔细看,其实就三层:状态定义、状态提升、副作用处理。

Compose 状态管理知识体系 状态定义 mutableStateOf remember / rememberSaveable StateFlow / MutableStateFlow 状态提升 ViewModel + StateFlow collectAsState() 状态向下传递 副作用处理 LaunchedEffect DisposableEffect SideEffect 核心原则 1. 状态尽量提升到最少的公共父节点 2. 用 StateFlow 连接 ViewModel 和 Compose,避免直接传可变对象 3. 副作用函数要选对:异步用 LaunchedEffect,资源清理用 DisposableEffect 4. 记住:remember 不抗进程重建,rememberSaveable 才抗 5. 状态更新用 update() 而不是直接赋值,避免并发问题

12.5 实战建议

说了这么多,总结几条我实际项目里的经验:

  • 简单页面:用 remember + mutableStateOf 就够了,别过度设计
  • 中等复杂度:ViewModel + StateFlow,配合 collectAsState()
  • 复杂页面:考虑用 StateFlow 做单向数据流,配合 sealed class 定义 UI 状态
  • 一次性事件:用 SharedFlow,别用 StateFlow

一个小技巧:

我习惯在 ViewModel 里定义一个 UiState data class,把所有页面状态都放进去。这样 Compose 里只需要 collect 一个 StateFlow,UI 逻辑特别清晰。重构时也方便,加字段就行,不用改接口。

好了,状态管理这块其实不难,关键是理解「状态在哪定义、怎么传递、副作用怎么处理」这三个问题。你写几个页面练练手,很快就能找到感觉。


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