20、ViewModel与Room:ViewModel简介、ViewModelProvider、数据暴露、屏幕旋转处理

好,咱们今天来聊聊 ViewModel。说实话,ViewModel 这玩意儿刚出来的时候,我第一反应是:这不就是 MVP 里的 Presenter 换个马甲吗?后来真在项目里用上了,才发现自己太年轻了。ViewModel 的核心价值,说白了就一句话:帮你在屏幕旋转时保住数据

你想想看,用户正填表单呢,手一滑手机横过来了——Activity 重建了,数据全没了。用户不骂娘才怪。ViewModel 就是来解决这个痛点的。

ViewModel 是什么?

ViewModel 是 Android Jetpack 里的一个组件。它的生命周期比 Activity 长。具体多长?只要 Activity 没真正 finish,ViewModel 就一直活着。屏幕旋转?那只是配置变更,ViewModel 纹丝不动。

我在项目中遇到过这样一个场景:用户在一个多步骤的注册流程里,已经填了三页信息,结果接了个电话,回来屏幕旋转了。如果没有 ViewModel,那三页数据全得重填。有了 ViewModel,数据稳稳地躺在那里,用户完全无感知。

核心要点:ViewModel 的生命周期范围是从 Activity 创建到 Activity 真正销毁(finish)。配置变更(如屏幕旋转)不会导致 ViewModel 销毁。

ViewModelProvider:怎么拿到 ViewModel?

你不能直接 new 一个 ViewModel。为什么?因为 ViewModel 的创建和缓存是由系统管理的。你需要通过 ViewModelProvider 来获取。

基本用法是这样的:

// 在 Activity 或 Fragment 中
val viewModel = ViewModelProvider(this).get(MyViewModel::class.java)

这里有个细节:ViewModelProvider(this) 里的 this 是谁?如果是 Activity,ViewModel 的作用域就是这个 Activity。如果是 Fragment,作用域就是这个 Fragment。我建议你尽量把 ViewModel 的作用域控制在 Activity 级别,这样 Fragment 之间共享数据更方便。

嗯,这里要注意:如果你在 Fragment 里用 ViewModelProvider(requireActivity()),那拿到的就是 Activity 级别的 ViewModel,多个 Fragment 可以共享同一个实例。这个技巧我在做多 Fragment 协作的页面时经常用。

数据暴露:LiveData 还是 StateFlow?

ViewModel 里怎么把数据暴露给 UI?传统做法是用 LiveData。现在更推荐用 StateFlow。我个人习惯用 StateFlow,因为它跟 Kotlin 协程配合得天衣无缝。

来看一个完整的例子:

class UserViewModel : ViewModel() {
    // 私有 MutableStateFlow,外部只能读不能写
    private val _userName = MutableStateFlow("")
    val userName: StateFlow<String> = _userName.asStateFlow()

    // 暴露给 UI 的数据
    val userDisplayName: StateFlow<String> = _userName
        .map { name ->
            if (name.isBlank()) "匿名用户"
            else "用户:$name"
        }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = "匿名用户"
        )

    fun updateName(name: String) {
        _userName.value = name
    }
}

这里有个坑,我曾经踩过:stateInstarted 参数。如果你用 SharingStarted.Eagerly,那即使没有订阅者,数据流也会一直运行。我建议用 WhileSubscribed(5000),这样当 UI 不再观察时,5 秒后自动停止,省资源。

小技巧:在 ViewModel 里尽量用 viewModelScope 启动协程。这个作用域会在 ViewModel 销毁时自动取消,避免内存泄漏。

屏幕旋转处理:ViewModel 的拿手好戏

屏幕旋转时,Activity 会走 onDestroy 然后重新 onCreate。但 ViewModel 不会。所以你在 ViewModel 里存的数据,旋转后依然在。

但这里有个细节:如果你的数据是从网络或数据库加载的,旋转后 ViewModel 不会自动重新加载。你需要自己控制加载时机。我一般这样处理:

class UserListViewModel(
    private val userRepository: UserRepository
) : ViewModel() {

    private val _users = MutableStateFlow<List<User>>(emptyList())
    val users: StateFlow<List<User>> = _users.asStateFlow()

    private var isLoaded = false

    fun loadUsers() {
        if (isLoaded) return  // 避免重复加载
        isLoaded = true

        viewModelScope.launch {
            val result = userRepository.getUsers()
            _users.value = result
        }
    }
}

为什么加个 isLoaded 标志?因为屏幕旋转后,Activity 重新创建,会再次调用 loadUsers()。如果没有这个标志,就会重复请求网络。我曾经在一个项目里没加这个判断,结果用户旋转一次屏幕,网络请求就多一次,被后台吐槽了。

警告:不要在 ViewModel 里持有 Activity 或 View 的引用。这会导致内存泄漏。ViewModel 的生命周期比 Activity 长,如果持有 Activity 引用,Activity 无法被回收。

ViewModel 与 Room 的配合

ViewModel 和 Room 是天作之合。Room 返回的 Flow 或 LiveData 可以直接暴露给 ViewModel,ViewModel 再转发给 UI。数据变化时,UI 自动更新。

class ProductViewModel(
    private val productDao: ProductDao
) : ViewModel() {

    // Room 返回 Flow,ViewModel 直接暴露
    val allProducts: StateFlow<List<Product>> = productDao.getAllProducts()
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = emptyList()
        )

    fun addProduct(product: Product) {
        viewModelScope.launch {
            productDao.insert(product)
        }
    }
}

你看,代码非常简洁。Room 的 Flow 是冷流,只有被收集时才会查询数据库。配合 WhileSubscribed,当 UI 不可见时自动停止监听,省电又省内存。

知识体系图

下面这张图帮你理清 ViewModel 在整个架构中的位置:

ViewModel 与 Room 协作架构 Activity / Fragment 观察 StateFlow / LiveData ViewModel • 持有 StateFlow / MutableStateFlow • 使用 viewModelScope 管理协程 调用方法 Repository 数据操作封装 Room Database 返回 Flow / LiveData 返回 Flow StateFlow 数据流向:UI → ViewModel → Repository → Room → ViewModel → UI

避坑指南

最后分享几个我踩过的坑:

  • 不要在 ViewModel 里存大量数据。ViewModel 虽然能活过屏幕旋转,但进程被杀还是会丢。重要数据要持久化到 Room 或 DataStore。
  • 不要用 AndroidViewModel 除非你需要 Application Context。很多人一上来就用 AndroidViewModel,其实大部分场景 ViewModel 就够了。
  • StateFlow 的 initialValue 要合理。如果初始值是空列表,UI 可能会闪一下空白。我一般用加载中的状态作为初始值。
  • 记得处理背压。Room 返回的 Flow 如果数据变化频繁,UI 可能来不及刷新。用 debouncedistinctUntilChanged 可以缓解。

好了,ViewModel 的核心内容就这些。说白了,它就是帮你管理 UI 相关数据的容器,让数据在配置变更时存活下来。配合 Room 的 Flow,数据驱动 UI 更新,代码写起来非常舒服。