4、ViewModel:ViewModel的创建与作用域、SavedStateHandle、ViewModelProvider.Factory

好,咱们今天来聊聊 ViewModel。说实话,这个组件是 Jetpack 里我最早爱上的一个。为什么?因为它解决了 Android 开发里一个老大难问题——屏幕旋转时数据丢失。

你想想看,以前我们写 Activity,一旋转手机,整个界面重建,所有临时数据全没了。要么用 onSaveInstanceState 存到 Bundle 里,要么干脆不让旋转。这两种方式都不优雅。ViewModel 的出现,算是把这个痛点彻底解决了。

ViewModel 的创建与作用域

ViewModel 的核心思想很简单:把数据从 Activity/Fragment 的生命周期中剥离出来。它会在 Activity 或 Fragment 对应的生命周期范围内保持存活。只要 Activity 没真正 finish,ViewModel 就一直存在。

我习惯用 by viewModels() 这个委托来创建,代码最简洁:

class MyActivity : AppCompatActivity() {
    private val viewModel: MyViewModel by viewModels()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewModel.loadData()
    }
}

这里有个关键点:作用域。ViewModel 的作用域取决于你获取它的方式:

  • Activity 作用域:在 Activity 里用 viewModels(),ViewModel 会跟随 Activity 的生命周期。
  • Fragment 作用域:在 Fragment 里用 viewModels(),ViewModel 只跟随当前 Fragment。
  • 共享作用域:在 Fragment 里用 activityViewModels(),多个 Fragment 可以共享同一个 ViewModel。

我在项目中遇到过这样一个场景:一个详情页里有三个 Tab Fragment,它们需要共享一份商品数据。如果每个 Fragment 各自创建 ViewModel,数据就不同步了。用 activityViewModels() 就完美解决。

核心要点:ViewModel 的生命周期比 Activity/Fragment 更长。它会在 Activity 的 onDestroy(非旋转导致的)时自动清理。所以千万别在 ViewModel 里持有 Activity 的引用,否则会内存泄漏。

SavedStateHandle:让 ViewModel 也能保存状态

好,问题来了。ViewModel 虽然能扛住屏幕旋转,但如果进程被系统杀了呢?比如用户切到后台,系统内存不足把你的 App 干掉了。这时候 ViewModel 也会消失,数据就丢了。

以前我们得手动在 onSaveInstanceState 里存数据,然后在 onCreate 里恢复。麻烦不说,还容易遗漏。

SavedStateHandle 就是来解决这个问题的。它本质上是一个 自动化的 Bundle 存储机制。你只需要在 ViewModel 的构造函数里声明它,框架会自动帮你保存和恢复数据。

class MyViewModel(
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {
    
    // 使用 SavedStateHandle 保存数据
    val userName = savedStateHandle.getLiveData<String>("user_name", "")
    
    fun updateName(name: String) {
        savedStateHandle["user_name"] = name
    }
}

嗯,这里要注意:SavedStateHandle 只能保存 Bundle 支持的数据类型。像自定义对象、大图片这些,你得自己序列化。我一般会把复杂对象转成 JSON 字符串再存。

小技巧:SavedStateHandle 还支持 getStateFlow() 方法,如果你用 Kotlin Flow 做数据流,可以直接拿到 StateFlow,省去手动转换的麻烦。

我曾经踩过一个坑:在 ViewModel 里同时用了 SavedStateHandle 和 Room 数据库。结果发现 SavedStateHandle 里存的数据和数据库里的不一致。后来才意识到,SavedStateHandle 只在进程被杀时恢复,而数据库是持久化的。所以我的建议是:临时状态用 SavedStateHandle,持久数据用 Room,别混着用。

ViewModelProvider.Factory:自定义创建逻辑

默认情况下,ViewModel 是通过无参构造函数创建的。但很多时候,我们需要给 ViewModel 传参数,比如 Repository、Application 上下文等。这时候就需要 ViewModelProvider.Factory 了。

说白了,Factory 就是一个 工厂模式,让你能控制 ViewModel 的创建过程。

class MyViewModelFactory(
    private val repository: MyRepository
) : ViewModelProvider.Factory {
    
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(MyViewModel::class.java)) {
            @Suppress("UNCHECKED_CAST")
            return MyViewModel(repository) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

使用的时候也很简单:

class MyActivity : AppCompatActivity() {
    private val viewModel: MyViewModel by viewModels {
        MyViewModelFactory(MyRepository())
    }
}

如果你用 Hilt 或 Koin 做依赖注入,其实很少需要手写 Factory。但理解它的原理很重要。我记得有一次排查一个 Bug,发现 ViewModel 里的数据总是旧的,查了半天才发现是 Factory 里每次都 new 了一个新的 Repository 实例,导致数据不同步。改成单例就好了。

避坑指南:千万不要在 Factory 里持有 Activity 或 Fragment 的引用。我曾经这么干过,结果导致内存泄漏,界面销毁了 ViewModel 还活着,Activity 对象却释放不掉。正确的做法是只传数据层对象,比如 Repository、UseCase 这些。

知识体系总览

下面这张图是我整理的 ViewModel 核心知识结构,你可以对照着回顾一下:

ViewModel 创建与作用域 • viewModels() - Activity/Fragment 作用域 • activityViewModels() - 共享作用域 • 生命周期跟随 Activity 的 onDestroy SavedStateHandle • 进程被杀时自动保存/恢复 • 支持 LiveData / StateFlow • 仅支持 Bundle 兼容类型 ViewModelProvider.Factory • 自定义 ViewModel 创建逻辑 • 支持构造函数传参 • 配合 Hilt/Koin 可省略 核心原则 1. ViewModel 生命周期长于 Activity/Fragment,不要持有 UI 引用 2. SavedStateHandle 用于进程被杀恢复,Room 用于持久化 3. Factory 里只传数据层对象,不传 UI 相关引用

总结一下

ViewModel 这三个知识点其实是一条线:创建方式决定了作用域,SavedStateHandle 解决了进程被杀的问题,Factory 让你能灵活传参。实际开发中,我建议你:

  • 能用 by viewModels() 就别手写 Factory,除非需要传参
  • 需要跨 Fragment 共享数据时,优先考虑 activityViewModels()
  • 涉及用户输入或临时状态,用 SavedStateHandle 兜底
  • 复杂数据持久化,交给 Room 或 DataStore

嗯,这些就是我这些年用 ViewModel 总结出来的经验。说白了,ViewModel 本身不复杂,但用好了能让你的代码结构清晰很多。下次咱们聊聊 LiveData 和 Flow 怎么选,那个话题更有意思。


公众号:蓝海资料掘金营,微信deep3321