3. ViewModel 作为状态容器:在 Compose 中集成 ViewModel、继承方式、状态暴露与生命周期
在 Compose 声明式 UI 中,状态管理是最核心的架构决策之一。ViewModel 作为 Android 官方推荐的架构组件,承担着“状态容器 + 生命周期感知”的双重角色。本章将深入 ViewModel 的集成方式、状态暴露模式、生命周期绑定原理,并通过完整示例展示如何在 Compose 中安全高效地使用 ViewModel。
3.1 ViewModel 在 Compose 中的角色定位
ViewModel 的本质是一个存储在 ViewModelStoreOwner(通常为 Activity 或 Fragment)范围内的类实例,其生命周期独立于屏幕旋转等配置变更,但会在所属作用域销毁时自动清除。在 Compose 中,ViewModel 通过 viewModel() 函数(来自 lifecycle-viewmodel-compose 库)被直接获取,不需要手动管理创建与销毁。
| 维度 | Activity / Fragment 传统方式 | Compose 方式 |
|---|---|---|
| 获取实例 | ViewModelProvider(this).get(MyViewModel::class.java) |
val vm: MyViewModel = viewModel() |
| 生命周期关联 | 手动传入 ViewModelStoreOwner | 自动使用最近的 ViewModelStoreOwner |
| 状态暴露 | LiveData / RxJava | StateFlow / MutableState |
| 配置变更 | 实例保留(默认) | 实例保留(默认) |
3.2 在 Compose 中集成 ViewModel:基础流程
集成 ViewModel 需要三个步骤:添加依赖 → 定义 ViewModel 类 → 在 Composable 中获取并使用。以下为完整的实现流程。
3.2.1 添加 Gradle 依赖
在模块级 build.gradle.kts 中添加以下依赖:
dependencies {
// Compose 基础
implementation("androidx.compose.ui:ui:1.6.0")
implementation("androidx.compose.material3:material3:1.2.0")
// ViewModel + Compose 集成
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
// 如果使用 SavedStateHandle(可选)
implementation("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.7.0")
}
lifecycle-viewmodel-compose 2.7.0 要求 Compose BOM 2024.01.00 以上,AGP 8.2+,Kotlin 1.9.0+。建议统一使用 BOM 管理 Compose 版本。
3.2.2 定义 ViewModel 类
推荐使用 StateFlow 暴露状态,保持与 Compose 的 collectAsState() 无缝配合。
class CounterViewModel : ViewModel() {
// 私有后备状态
private val _count = MutableStateFlow(0)
// 公开不可变的 StateFlow
val count: StateFlow<Int> = _count.asStateFlow()
// 业务方法(更新状态)
fun increment() {
_count.update { it + 1 }
}
fun decrement() {
_count.update { it - 1 }
}
}
3.2.3 在 Composable 中获取 ViewModel 并观察状态
@Composable
fun CounterScreen() {
// viewModel() 默认使用最近的 ViewModelStoreOwner(Activity / Fragment)
val vm: CounterViewModel = viewModel()
// collectAsState() 将 StateFlow 转换为 Compose State
val count by vm.count.collectAsState()
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "当前计数: $count", style = MaterialTheme.typography.headlineMedium)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(onClick = { vm.increment() }) { Text("+1") }
Button(onClick = { vm.decrement() }) { Text("-1") }
}
}
}
3.3 ViewModel 的继承方式与扩展
ViewModel 类本身是抽象类,推荐通过继承 + 构造函数传入参数的方式扩展。常见的继承模式包括:
- 基础 ViewModel:直接继承
ViewModel(),无参数,适合简单场景。 - 带参数的 ViewModel:通过
SavedStateHandle或自定义ViewModelProvider.Factory传入初始参数。 - ViewModel + Repository 注入:通过
ViewModelProvider.Factory传入仓库实例,实现依赖分离。
3.3.1 使用 SavedStateHandle 保存界面状态
class CounterWithSavedStateViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
// SavedStateHandle 自动在配置变更后恢复数据
private val _count = savedStateHandle.getStateFlow("count", 0)
val count: StateFlow<Int> = _count
fun increment() {
savedStateHandle["count"] = (_count.value + 1)
}
}
// 获取方式不变:viewModel() 自动处理 SavedStateHandle 的注入
@Composable
fun SavedCounterScreen() {
val vm: CounterWithSavedStateViewModel = viewModel()
val count by vm.count.collectAsState()
// ... UI 相同
}
3.3.2 自定义 Factory 注入 Repository
class UserRepository(private val : String) {
fun getUser(): User = User(name = "Alice", age = 30)
}
class UserViewModel(
private val repository: UserRepository
) : ViewModel() {
private val _user = MutableStateFlow(repository.getUser())
val user: StateFlow<User> = _user.asStateFlow()
}
// 自定义 Factory
class UserViewModelFactory(private val : String) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
val repository = UserRepository()
@Suppress("UNCHECKED_CAST")
return UserViewModel(repository) as T
}
}
// 在 Composable 中使用
@Composable
fun UserScreen() {
val factory = remember { UserViewModelFactory("secret-key") }
val vm: UserViewModel = viewModel(factory = factory)
val user by vm.user.collectAsState()
// ... 渲染用户信息
}
remember 中的 Factory 实例不应在重组时重建,否则会导致 ViewModel 被重新创建。如果 Factory 不依赖 Composable 局部变量,建议在 ViewModelStoreOwner 作用域外创建单例。
3.4 状态暴露原则:单向数据流与不可变接口
ViewModel 对外应只暴露不可变的 StateFlow 或 State,内部使用 MutableStateFlow 更新。这保证了 单向数据流:UI 只读取状态,不直接修改;所有变更通过 ViewModel 的公共方法触发。
// 反例:直接暴露 MutableStateFlow(破坏封装)
class BadViewModel : ViewModel() {
val count = MutableStateFlow(0) // 外部可以调用 count.value = 1
}
// 正确做法
class GoodViewModel : ViewModel() {
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count.asStateFlow()
fun setValue(newValue: Int) {
_count.value = newValue
}
}
3.5 ViewModel 的生命周期与 Compose 的重组
ViewModel 的生命周期由 ViewModelStoreOwner 控制,而 Compose 的 viewModel() 函数只负责获取已存在的实例或创建新实例,不会产生额外生命周期开销。关键生命周期节点如下:
1. ViewModel 在所属 Activity/Fragment 的
onCreate 之后首次创建。2. 配置变更(如旋转屏幕)时 ViewModel 实例保留,Compose 重组但 ViewModel 状态不丢失。
3. Activity 被系统销毁(如资源不足)时,即使没有 finish,ViewModel 也会被销毁。
4. 当
ViewModelStoreOwner 的 onCleared() 被调用后,ViewModel 不可再被获取。
Compose 重组时,viewModel() 不会重复创建新实例,因为底层使用了 remember 和 LocalViewModelStoreOwner 的组合。每次重组仅重新收集 StateFlow 的值,不会触发 ViewModel 初始化代码。
3.6 调试与常见错误
3.6.1 常见错误与解决方案
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
| ViewModel 每次重组都重建 | 没有为带参数的 ViewModel 提供正确的 Factory | 使用 viewModel(factory = remember { ... }) 传入稳定的 Factory |
| 状态在后台进程被杀后丢失 | 未使用 SavedStateHandle 或持久化 | 使用 SavedStateHandle 保存界面关键状态 |
StateFlow 在 Compose 中不更新 UI |
未使用 collectAsState() 或使用了 asStateFlow() 但收集了旧的 Flow |
确保在 LaunchedEffect 或 collectAsState() 中收集 |
| 多个 ViewModel 实例共存 | 在多个嵌套 Composable 中分别调用 viewModel(),且处于不同 ViewModelStoreOwner |
在最高层级获取一次 ViewModel,并通过参数向下传递(状态提升) |
3.6.2 调试技巧
- 在 ViewModel 的
init块和onCleared()中添加日志,跟踪实例创建与销毁。 - 使用
Lifecycle.Event观察 ViewModelStoreOwner 的生命周期变化。 - 在 Composable 中使用
SideEffect { Log.d("TAG", "recompose") }检查重组频率。
3.7 内联 SVG:ViewModel 生命周期与状态流关系图
3.8 性能与稳定性优化建议
- 避免在 ViewModel 中持有 Compose 引用:如
Context、View,否则可能导致内存泄漏。 - 使用
viewModelScope管理协程:ViewModel 内置的viewModelScope会在onCleared()时自动取消协程,防止后台泄漏。 - 谨慎使用
MutableState代替StateFlow:MutableState是 Compose 专用的快照状态,更新会立即触发重组;StateFlow则是 Kotlin 协程流的实现,更适合与 Repository 层交互。在纯 ViewModel 内部推荐StateFlow,保持一致性。 - 将大量计算逻辑放在 ViewModel 工作线程:使用
viewModelScope.launch(Dispatchers.IO)避免阻塞主线程,并通过StateFlow将结果传回 UI 线程。 - 测试覆盖:ViewModel 单元测试无需 Android 环境,可通过
kotlinx-coroutines-test模拟StateFlow的发射与收集。
3.9 本章总结
ViewModel 在 Compose 中的集成已经标准化:通过 viewModel() 函数获取、StateFlow 暴露状态、collectAsState() 在 Composable 中观察。关键要点包括:
- 始终坚持不可变接口
StateFlow,禁止暴露MutableStateFlow。 - 使用
SavedStateHandle或自定义 Factory 应对参数化构造场景。 - 理解 ViewModel 生命周期独立于 Composable 重组,但受制于
ViewModelStoreOwner。 - 通过
viewModelScope管理协程,避免泄漏。
掌握这些原则后,即可在真实项目中构建稳定、可测试的 Compose + ViewModel 状态管理方案。