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 生命周期与状态流关系图

ViewModel 生命周期与 Compose 状态流 1. Activity onCreate 2. 创建 ViewModel 实例 3. 保留实例(配置变更) 4. onCleared() 销毁 MutableStateFlow<T> asStateFlow() 公开 collectAsState() Compose UI 重组 ViewModel 持有 UI 事件触发 ViewModel 方法更新状态 状态在 ViewModel 中保留,不随重组丢失

3.8 性能与稳定性优化建议

  1. 避免在 ViewModel 中持有 Compose 引用:如 Context、View,否则可能导致内存泄漏。
  2. 使用 viewModelScope 管理协程:ViewModel 内置的 viewModelScope 会在 onCleared() 时自动取消协程,防止后台泄漏。
  3. 谨慎使用 MutableState 代替 StateFlow:MutableState 是 Compose 专用的快照状态,更新会立即触发重组;StateFlow 则是 Kotlin 协程流的实现,更适合与 Repository 层交互。在纯 ViewModel 内部推荐 StateFlow,保持一致性。
  4. 将大量计算逻辑放在 ViewModel 工作线程:使用 viewModelScope.launch(Dispatchers.IO) 避免阻塞主线程,并通过 StateFlow 将结果传回 UI 线程。
  5. 测试覆盖:ViewModel 单元测试无需 Android 环境,可通过 kotlinx-coroutines-test 模拟 StateFlow 的发射与收集。

3.9 本章总结

ViewModel 在 Compose 中的集成已经标准化:通过 viewModel() 函数获取、StateFlow 暴露状态、collectAsState() 在 Composable 中观察。关键要点包括:

  • 始终坚持不可变接口 StateFlow,禁止暴露 MutableStateFlow。
  • 使用 SavedStateHandle 或自定义 Factory 应对参数化构造场景。
  • 理解 ViewModel 生命周期独立于 Composable 重组,但受制于 ViewModelStoreOwner。
  • 通过 viewModelScope 管理协程,避免泄漏。

掌握这些原则后,即可在真实项目中构建稳定、可测试的 Compose + ViewModel 状态管理方案。