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 真正 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