4. ViewModel与状态管理:ViewModel的创建与作用域、ViewModelProvider与ViewModelStore原理、SavedStateHandle处理进程重建、跨Fragment共享ViewModel
各位同学,今天我们聊一个非常核心的话题——ViewModel。说实话,ViewModel 是 Jetpack 组件里我最早拥抱的一个。为什么?因为它在处理屏幕旋转、进程重建这些场景时,真的太省心了。
但很多同学只是会用,并不清楚它背后的原理。比如 ViewModel 到底存在哪里?为什么跨 Fragment 能共享同一个 ViewModel?进程被杀后数据怎么恢复?今天我们就把这些事彻底讲透。
核心要点:ViewModel 不是用来存大数据的,它的核心价值是帮你在配置变更(如屏幕旋转)时保住数据,以及在 Activity 重建时自动恢复。
4.1 ViewModel 的创建与作用域
先看一个最简单的用法:
class MyViewModel : ViewModel() {
private val _count = MutableLiveData<Int>(0)
val count: LiveData<Int> = _count
fun increment() {
_count.value = (_count.value ?: 0) + 1
}
}
// 在 Activity 中使用
val viewModel = ViewModelProvider(this).get(MyViewModel::class.java)
这段代码大家应该很熟悉。但我想问一个问题:ViewModelProvider(this) 里的 this 是什么?
答案是 ViewModelStoreOwner。Activity 和 Fragment 都实现了这个接口。ViewModel 的作用域就由这个 owner 决定。
- Activity 作用域:ViewModel 存活于 Activity 的整个生命周期,包括 onDestroy 后的重建过程。
- Fragment 作用域:ViewModel 存活于 Fragment 的生命周期,Fragment 销毁后 ViewModel 也会被清理。
- Activity 级别的 Fragment 共享:如果传入 activity 作为 owner,多个 Fragment 可以共享同一个 ViewModel。
我的经验:我个人习惯把 ViewModel 的作用域尽量缩小。能用 Fragment 级别的,就不用 Activity 级别的。因为作用域越大,内存占用时间越长,容易造成不必要的内存泄漏。
4.2 ViewModelProvider 与 ViewModelStore 原理
好,现在我们来扒一扒 ViewModelProvider 的底层。说白了,它就是一个工厂 + 缓存的管理器。
核心流程是这样的:
- ViewModelProvider 从 ViewModelStoreOwner 中拿到 ViewModelStore。
- ViewModelStore 内部维护了一个 HashMap<String, ViewModel>。
- 通过 key(默认是类名)去 HashMap 中查找,如果找到就直接返回。
- 如果没找到,就通过 Factory 创建新的 ViewModel,存入 HashMap,再返回。
看源码会更清楚:
// ViewModelProvider 的核心逻辑(简化版)
public class ViewModelProvider {
private final ViewModelStore store;
private final Factory factory;
public <T extends ViewModel> T get(Class<T> modelClass) {
String canonicalName = modelClass.getCanonicalName();
// 先从缓存中取
ViewModel viewModel = store.get(canonicalName);
if (modelClass.isInstance(viewModel)) {
return (T) viewModel;
}
// 缓存中没有,就创建
viewModel = factory.create(modelClass);
store.put(canonicalName, viewModel);
return (T) viewModel;
}
}
那 ViewModelStore 又存在哪里呢?答案是 Activity 的 NonConfigurationInstances 中。这个对象在 Activity 重建时会被保留下来。所以 ViewModel 才能躲过屏幕旋转的「劫难」。
注意:我曾经遇到过一个坑——在 Fragment 中通过 ViewModelProvider(requireActivity()) 获取 ViewModel,结果 Fragment 被销毁重建后,ViewModel 里的数据还在。这其实是符合预期的,因为作用域是 Activity 级别的。但如果你没搞清楚作用域,就会觉得「数据怎么没清掉?」
4.3 SavedStateHandle 处理进程重建
屏幕旋转这种配置变更,ViewModel 能轻松应对。但如果是进程被系统杀掉了呢?这时候 ViewModelStore 也会被销毁,数据就丢了。
怎么办?SavedStateHandle 就是来解决这个问题的。
它的原理很简单:ViewModel 通过 SavedStateHandle 来读写数据,这些数据会被自动保存到 Bundle 中,进程重建后从 Bundle 恢复。
class MyViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
// 直接从 SavedStateHandle 中读取,自动保存
val count = savedStateHandle.getLiveData<Int>("count", 0)
fun increment() {
count.value = (count.value ?: 0) + 1
}
}
// 创建时不需要手动传参,ViewModelProvider 会自动注入
val viewModel = ViewModelProvider(this).get(MyViewModel::class.java)
这里有个细节:ViewModel 的构造函数需要接收 SavedStateHandle,但 ViewModelProvider 会自动帮你注入。前提是你没有自定义 Factory,或者你的 Factory 继承了 AbstractSavedStateViewModelFactory。
避坑指南:我曾经在项目里自定义了 ViewModelProvider.Factory,结果忘了处理 SavedStateHandle,导致进程重建后数据全丢了。排查了半天才发现是 Factory 的问题。所以如果你要自定义 Factory,记得继承 AbstractSavedStateViewModelFactory。
SavedStateHandle 支持的数据类型和 Bundle 一样,比如 String、Int、Parcelable、Serializable 等。但要注意,它不适合存大数据,比如 Bitmap 或者大的 List。因为 Bundle 有 1MB 的大小限制。
4.4 跨 Fragment 共享 ViewModel
这个场景在实际开发中非常常见。比如一个主界面有列表 Fragment 和详情 Fragment,它们需要共享同一个数据源。
实现方式很简单:两个 Fragment 都使用同一个 Activity 作为 ViewModelStoreOwner。
// Fragment A
val sharedViewModel = ViewModelProvider(requireActivity()).get(SharedViewModel::class.java)
// Fragment B
val sharedViewModel = ViewModelProvider(requireActivity()).get(SharedViewModel::class.java)
这样 Fragment A 和 Fragment B 拿到的是同一个 ViewModel 实例。数据变化会实时同步。
但这里有一个容易踩的坑:Fragment 的生命周期问题。
假设 Fragment A 先创建,它通过 requireActivity() 获取了 SharedViewModel。然后 Fragment B 被添加,也获取了同一个 ViewModel。一切正常。
但如果 Fragment A 被销毁了(比如从返回栈中移除),而 Fragment B 还在,这时候 SharedViewModel 依然存在,因为它的作用域是 Activity。但 Fragment A 中通过 LiveData 观察的数据,会因为 Fragment A 的销毁而自动解绑,不会造成内存泄漏。
我的建议:跨 Fragment 共享 ViewModel 时,最好在 ViewModel 中使用 LiveData 或 StateFlow 来暴露数据。这样 Fragment 在 onDestroy 时会自动取消订阅,避免内存泄漏。我见过有人直接用 MutableLiveData 的 setValue 来通知,结果 Fragment 销毁后还在更新 UI,导致空指针异常。
另外,如果你使用 Navigation 组件,还可以通过 navGraphViewModels 来限定 ViewModel 的作用域到某个导航图。这样只有该导航图内的 Fragment 才能共享这个 ViewModel。
// 在 Fragment 中获取导航图级别的 ViewModel
val viewModel: SharedViewModel by navGraphViewModels(R.id.nav_graph_id)
这种方式比 requireActivity() 更精确,不会污染整个 Activity 的 ViewModel 空间。
4.5 知识体系总览
为了让大家更直观地理解 ViewModel 的整个体系,我画了一张图:
4.6 小结
好了,关于 ViewModel 的核心内容就讲到这里。我们回顾一下:
- ViewModel 的创建依赖于 ViewModelStoreOwner,作用域由 owner 决定。
- ViewModelProvider 通过 ViewModelStore 缓存 ViewModel,配置变更时数据不丢。
- SavedStateHandle 解决进程重建问题,数据自动保存到 Bundle。
- 跨 Fragment 共享 ViewModel 时,注意作用域和生命周期管理。
嗯,这些内容看起来不少,但说白了就是一句话:ViewModel 帮你管好数据,你只管写业务逻辑。下一章我们会聊 LiveData 和 StateFlow 的对比,到时候你会看到 ViewModel 和它们配合起来有多强大。
公众号:蓝海资料掘金营,微信deep3321