1. ViewModel在Compose中的角色定位

1. 核心职责与架构原理

在 Jetpack Compose 体系中,ViewModel 不再仅仅是传统 MVC 中的“控制器”,而是经过深度改造的状态容器与生命周期边界。它的三个核心职责——生命周期感知、状态持有、与 UI 解耦——共同构成 Compose 声明式 UI 稳定运行的基石。

  • 生命周期感知: ViewModel 的默认作用域绑定到 NavBackStackEntry 或 Activity。当 Composable 因配置变更(如屏幕旋转)而经历重组或销毁时,ViewModel 实例在内存中保持不变,直到其作用域被系统永久销毁。
  • 状态持有: 作为单一可信数据源,ViewModel 通过 StateFlow、MutableState 或 LiveData 对外暴露不可变状态。Compose 通过 collectAsState() 订阅这些状态,自动触发重组。
  • 与 UI 解耦: ViewModel 不持有任何 Context(除非使用 AndroidViewModel,且有滥用风险)或 Composable 引用。它通过事件驱动模型(如密封类定义 UiAction)与 UI 层通信,实现逻辑层的平台无关测试。

2. 架构流程与实现步骤

以下流程图直观展示了 ViewModel 在 Compose 架构中的桥梁位置,以及状态与事件的流动方向:

ViewModel 生命周期感知与状态持有架构 组合体 (Compose) 配置变更/重建 Activity/Fragment ViewModel (作用域内) 仓库/数据层 订阅状态 (collectAsState) 作用域绑定 挂起函数/Flow 核心洞察:ViewModel 在配置变更后幸存,保持状态不丢失;Compose 通过事件向上传递,状态向下流动,实现完全解耦。

3. 关键实现代码与API

步骤 1: 定义 ViewModel,使用 StateFlow 持有不可变状态。

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

    fun onAction(action: UiAction) {
        when (action) {
            is UiAction.LoadData -> {
                viewModelScope.launch {
                    _uiState.value = UiState(isLoading = true)
                    // 调用仓库层
                }
            }
        }
    }
}

data class UiState(val isLoading: Boolean = false, val data: List<String> = emptyList())
sealed class UiAction {
    object LoadData : UiAction()
}

步骤 2: 在 Composable 中注入 ViewModel 并收集状态。

@Composable
fun MyScreen(viewModel: MyViewModel = viewModel()) {
    val uiState by viewModel.uiState.collectAsState()
    
    if (uiState.isLoading) {
        CircularProgressIndicator()
    } else {
        LazyColumn {
            items(uiState.data) { item ->
                Text(text = item)
            }
        }
    }
}

4. 生命周期与解耦对比

特征ViewModel普通类SavedStateHandle
配置变更存活是否是(序列化)
作用域控制viewModelScope无附加在ViewModel上
UI依赖无(零引用)通常持有引用无
单元测试友好高低中

5. 常见错误与稳定性优化

错误模式 1:Scope 泄露。 在 init { } 块中使用 viewModelScope.launch 时未捕获异常,或错误地传入 Context 导致内存泄漏。
✅ 优化: 始终在 viewModelScope 内启动协程;必须使用 Context 时,优先使用 AndroidViewModel 并谨慎评估生命周期。
调试技巧: 开启 ViewModel 日志追踪。在 onCleared() 以及状态 _uiState.value 赋值处添加日志,验证生命周期与状态更新时序。
错误模式 2:暴露可变数据。 直接暴露 MutableStateFlow 给 UI 层,导致状态在 ViewModel 外部被非法修改。
✅ 优化: 永远对外暴露不可变的 StateFlow,使用 .asStateFlow() 包装。
性能建议: 避免在 ViewModel 中进行大量计算。将复杂转换操作放在仓库层或使用 map 操作符。利用 stateIn 操作符将冷流转化为热流,避免数据被多次 collect 时重复执行昂贵计算。