4、ViewModel:ViewModel原理、ViewModel创建、ViewModelProvider、SavedStateHandle、实战:屏幕旋转数据保持。

好,咱们今天来聊聊 ViewModel。说实话,这个组件是我在 Jetpack 里最早爱上的一个。为什么?因为它解决了一个特别烦人的问题——屏幕旋转。

你想想看,以前写 Android 应用,最头疼的就是屏幕一转,Activity 重建,数据全没了。要么用 onSaveInstanceState 存 Bundle,要么自己搞个 Fragment 保活。麻烦不说,代码还乱成一锅粥。ViewModel 的出现,说白了就是让数据在配置变更时「活下来」。

ViewModel 的核心原理

ViewModel 为什么能存活?它的秘密藏在 ViewModelStore 里。

每个 Activity 或 Fragment 都关联着一个 ViewModelStore。当配置变更(比如屏幕旋转)时,系统不会销毁旧的 ViewModelStore,而是把它传递给新的 Activity 实例。ViewModel 就住在这个 Store 里,所以它活下来了。

我画了一张图,帮你理清这个关系:

ViewModel 生命周期与存储原理 Activity 创建 屏幕旋转 Activity 重建 ViewModelStore ViewModel 实例 ViewModel 实例 ViewModel 实例 获取/创建 复用 复用 关键点: • Activity 重建时 ViewModelStore 不销毁 • ViewModel 实例被复用 • 数据自然保持

嗯,这里要注意:ViewModel 不是不销毁,而是延迟销毁。只有当 Activity 真正 finish 了,或者 Fragment 被 detach 了,ViewModelStore 才会被清空。所以别指望 ViewModel 能跨 Activity 存活,它只活在当前 Activity 的生命周期里。

ViewModel 的创建方式

创建 ViewModel 其实很简单。我个人习惯用 ViewModelProvider,这是官方推荐的方式。

// 最简单的创建
val viewModel = ViewModelProvider(this).get(MyViewModel::class.java)

// 带 Factory 的创建(需要传参时)
val viewModel = ViewModelProvider(
    this,
    MyViewModelFactory("参数")
).get(MyViewModel::class.java)

为什么需要 Factory?因为 ViewModel 的构造函数默认不能带参数。如果你需要传参,就得自己写一个 Factory。我在项目中遇到过这种情况:用户登录后需要把 userId 传给 ViewModel,这时候 Factory 就派上用场了。

核心要点:ViewModelProvider 会先检查 ViewModelStore 里有没有实例。有就直接返回,没有就通过 Factory 创建。这就是「复用」的机制。

ViewModelProvider 的细节

ViewModelProvider 其实是个工具类。它内部持有 ViewModelStore 和 Factory。每次调用 get() 方法时,它都会去 Store 里找。找到了就返回,找不到就创建。

我整理了一个表格,帮你对比不同场景下的使用方式:

场景 使用方式 说明
无参构造函数 ViewModelProvider(this).get(XxxViewModel::class.java) 最简单,默认 Factory
有参构造函数 自定义 Factory,实现 ViewModelProvider.Factory 接口 需要手动创建 Factory 实例
Fragment 共享数据 ViewModelProvider(requireActivity()).get(XxxViewModel::class.java) 传入 Activity 实例,实现 Fragment 间共享
Hilt 注入 @HiltViewModel + by viewModels() 最省事,但需要引入 Hilt

我的建议:如果项目不大,直接用 ViewModelProvider 就行。如果项目大了,可以考虑 Hilt,它能自动帮你处理 Factory 的创建,省心不少。

SavedStateHandle:让数据更持久

ViewModel 虽然能应对屏幕旋转,但遇到进程被系统杀死的情况,它也无能为力。这时候就需要 SavedStateHandle 出马了。

SavedStateHandle 是 ViewModel 里的一个特殊对象。它内部维护了一个 Bundle,可以自动保存和恢复数据。说白了,它就是 onSaveInstanceState 的升级版,但用起来更优雅。

class MyViewModel(
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {

    // 使用 SavedStateHandle 保存数据
    val count = savedStateHandle.getLiveData<Int>("count", 0)

    fun increment() {
        count.value = (count.value ?: 0) + 1
    }
}

你看,代码里直接通过 savedStateHandle.getLiveData() 获取数据。系统会自动帮你保存和恢复。我曾经在一个电商项目里用过这个特性,用户选了半天商品,结果一个电话进来进程被杀,回来数据还在。用户体验好了不少。

注意:SavedStateHandle 只能保存 Bundle 支持的数据类型。如果你要保存复杂对象,要么序列化,要么用 Room 数据库。别想着把整个 Bitmap 塞进去,会炸的。

实战:屏幕旋转数据保持

说了这么多,咱们来写个完整的例子。这个例子很简单:一个计数器,屏幕旋转后数据不丢失。

// 1. 定义 ViewModel
class CounterViewModel(
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {

    private val _count = savedStateHandle.getLiveData<Int>("count", 0)
    val count: LiveData<Int> get() = _count

    fun increment() {
        _count.value = (_count.value ?: 0) + 1
    }
}

// 2. 在 Activity 中使用
class MainActivity : AppCompatActivity() {

    private lateinit var viewModel: CounterViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        viewModel = ViewModelProvider(this).get(CounterViewModel::class.java)

        val tvCount = findViewById<TextView>(R.id.tv_count)
        val btnIncrement = findViewById<Button>(R.id.btn_increment)

        // 观察数据变化
        viewModel.count.observe(this) { value ->
            tvCount.text = "计数:$value"
        }

        btnIncrement.setOnClickListener {
            viewModel.increment()
        }
    }
}

这段代码跑起来后,你旋转屏幕试试。计数不会归零,因为 ViewModel 活下来了。就算进程被杀死,SavedStateHandle 也能帮你恢复数据。

嗯,这里有个小坑:如果你用 Fragment,记得在 Fragment 里获取 ViewModel 时传入 requireActivity(),这样才能让 Fragment 共享同一个 ViewModel 实例。我之前有个同事没注意这个,结果每个 Fragment 都创建了自己的 ViewModel,数据根本不同步,排查了半天。

最后总结一下:ViewModel 的核心是 ViewModelStore,它让数据在配置变更时存活。SavedStateHandle 是它的补充,应对进程被杀的情况。两者配合使用,基本能覆盖大部分数据保持的场景。

好了,这一节就到这里。记住,写代码时多想想「这个数据该放哪」,养成好习惯。


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