3. ViewModel状态暴露方式

在 Compose 与 ViewModel 协作时,如何将 ViewModel 内部的状态安全、高效地暴露给 Composable 是决定响应式更新性能的关键。Android 生态中常用的三种状态载体——LiveData、StateFlow 和 MutableState——各有设计目标和适用场景。本章从原理出发,对比三者的生命周期行为、线程模型、转换开销,并给出在 Compose 中的具体实现方案与优化策略。

ViewModel 状态暴露路径 ViewModel 状态 → Compose 的三种暴露路径 ViewModel LiveData StateFlow MutableState observeAsState() collectAsState() by delegate State<T> State<T> State<T> @Composable UI LiveData / StateFlow / MutableState 三者在 ViewModel 中创建,通过不同的适配方式转换为 Compose 的 State,最终驱动 UI 重组

3.1 三种暴露方式的核心原理

特性LiveDataStateFlowMutableState
所属体系Android Architecture ComponentsKotlin coroutines + FlowJetpack Compose runtime
生命周期感知自动感知 LifecycleOwner需配合 repeatOnLifecycle无生命周期感知(Compose 自动管理 recomposition)
线程安全主线程设置值任意线程,默认并发安全主线程修改(Compose 快照系统)
冷/热流热(有缓存值)热(有初始值,保留最新值)热(Compose 状态)
与 Compose 集成方式observeAsState()collectAsState()直接委托 by 或 remember
防抖/去重无内置去重可结合 distinctUntilChanged()快照系统自动去重
单元测试难度需要 mock LifecycleOwner直接测试 Flow直接测试 Compose 状态
推荐策略:新项目优先使用 StateFlow,配合 stateIn 在 ViewModel 中转换为共享流;纯 ViewModel 控制 UI 状态且无需复杂转换时用 MutableState;现有 LiveData 代码可保留,但避免在新 Compose 模块中新增。

3.2 实现流程与代码示例

3.2.1 方式一:LiveData + observeAsState()

ViewModel 中暴露 LiveData,Composable 通过扩展函数 observeAsState() 转换为 Compose 的 State。

// ViewModel
class ProfileViewModel : ViewModel() {
    private val _name = MutableLiveData("Guest")
    val name: LiveData<String> = _name

    fun updateName(newName: String) {
        _name.value = newName
    }
}

// Composable
@Composable
fun ProfileScreen(viewModel: ProfileViewModel) {
    val name by viewModel.name.observeAsState("Guest")
    Text("Hello, $name")
    Button(onClick = { viewModel.updateName("Alice") }) {
        Text("Change Name")
    }
}
注意:observeAsState 在 Composable 离开 Composition 时自动取消订阅,但 LiveData 本身需要在主线程设值,且无法天然应对背压。在生命周期的 onPause 后仍可能触发短暂更新。

3.2.2 方式二:StateFlow + collectAsState()

ViewModel 使用 MutableStateFlow 持有状态,Composable 使用 collectAsState() 收集。

// ViewModel
class SearchViewModel : ViewModel() {
    private val _query = MutableStateFlow("")
    val query: StateFlow<String> = _query.asStateFlow()

    fun onQueryChanged(newQuery: String) {
        _query.value = newQuery
    }
}

// Composable
@Composable
fun SearchBar(viewModel: SearchViewModel) {
    val query by viewModel.query.collectAsState()
    TextField(value = query, onValueChange = { viewModel.onQueryChanged(it) })
}
最佳实践:使用 asStateFlow() 暴露出不可变的 StateFlow,避免外部直接修改。配合 stateIn(WhileSubscribed(5000)) 可处理上游冷流转热流的生命周期策略。

3.2.3 方式三:MutableState + 委托(直接使用 Compose 状态)

ViewModel 内部直接持有 MutableState,Composable 通过属性委托读取。

// ViewModel
class CounterViewModel : ViewModel() {
    var count by mutableStateOf(0)
        private set

    fun increment() { count++ }
}

// Composable
@Composable
fun CounterScreen(viewModel: CounterViewModel) {
    Text("Count: ${viewModel.count}")
    Button(onClick = { viewModel.increment() }) {
        Text("+1")
    }
}
关键约束:直接使用 MutableState 的 ViewModel 必须明确 ViewModel 属于 Compose 范围,且状态只在 Compose 内观察;若需在非 Compose 场景(如 Service、BroadcastReceiver)访问,应改用 StateFlow。

3.3 状态转换与互操作性

实际项目中常需要相互转换:

  • LiveData → Flow:liveData.asFlow() (ktx 扩展)
  • Flow → LiveData:flow.asLiveData()
  • Flow → State:stateIn(scope, SharingStarted.WhileSubscribed(), initialValue)
  • State → Flow:snapshotFlow { state.value }
// 将 Room Dao 的 Flow 转换为 ViewModel 中的 StateFlow,并暴露给 Compose
class ProductViewModel(productId: String) : ViewModel() {
    private val repo = ProductRepository()

    val product: StateFlow<Product?> = repo.getProductFlow(productId)
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = null
        )
}

3.4 调试与常见错误

问题原因解决方法
Compose 不重组StateFlow 值未变化(distinctUntilChanged 过滤)确保每次赋值新对象,或手动使用 .update { } 触发
LiveData 在 Compose 外触发更新忘记加 @MainThread 或使用 postValue使用 viewModelScope.launch(Dispatchers.Main) { _liveData.value = ... }
MutableState 导致 ViewModel 无法被序列化状态对象包含 State 引用ViewModel 内部使用普通属性,暴露时再包装为 State
流泄漏(Flow not cancelled)未在 ViewModel 关闭时取消始终使用 viewModelScope 或 WhileSubscribed
性能提示:对于高频更新(如滚动位置、动画进度),优先使用 MutableState 避免 Flow 额外开销;对于跨模块状态或需保留最新值的异步操作,使用 StateFlow 配合 distinctUntilChanged 减少不必要的重组。

3.5 稳定性与优化要点

  • 避免在 ViewModel 中暴露 MutableState 给多个 Composable:每个 Composable 独立订阅,推荐使用 derivedStateOf 做派生状态,减少全局快照锁定。
  • 使用 SharingStarted.WhileSubscribed(5000):当屏幕旋转时,上游 Flow 在 5 秒内不被取消,避免频繁创建数据库查询游标。
  • LiveData 在 Compose 中应作为过渡方案:新代码尽量改为 StateFlow,以利用 Flow 的 operators(map、flatMapLatest、combine)简化状态组合。
  • 单元测试直接使用 runTest:对 StateFlow 可使用 .first()、.toList() 验证,对 MutableState 直接访问属性。