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 架构中的桥梁位置,以及状态与事件的流动方向:
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 时重复执行昂贵计算。