Android Jetpack架构组件——一文带你了解ViewModel的使用及原理
概述
前面我们讲过了lifecycle的使用及原理。今天我们谈谈viewModel。原本使用和原理是准备分开写的,结果我看了下ViewModel的原理,很简单,所以决定把两者放在一起了。那么接下来,我们进入正题。
ViewModel是什么?
ViewModel旨在以注重生命周期的方式存储和管理界面相关的数据。我们知道当屏幕旋转时,Activity会销毁并且重建,而它让数据可在发生屏幕旋转等配置更改后继续留存。
哎?那就有人要问了,为什么我们不通过onSaveInstanceState()对数据进行保存,然后在onCreate()的时候读取数据呢?这种方法其实只适合少量的数据,并且它还需要进行序列化操作。不过毕竟Bundle的传输数据是有大小限制的。
还有Activity和Fragment有数据交互的时候,那么我们的成本其实也是相对有点高。而ViewModel便可以替我们解决此类问题。
所以从UI控制器逻辑中分离出View的展示数据所有权的操作更容易且更高效。
ViewModel的生命周期
我们先看一张官网的图:
上图说明了Activity经历屏幕旋转而后结束时所处的各种生命周期状态。并且在Activity生命周期旁边显示了对应的ViewModel的生命周期。此图只展示了Activity相关的生命周期,而在Fragment上其实一样。
通常来说,我们获取一个ViewModel是在Activity的onCreate()中去获取的,但onCreate()方法可能被调用多次,比如屏幕旋转,所以ViewModel的存在时间其实是第一次获取实例到当前页面完全销毁。
ViewModel的使用
那么现在我们准备用ViewModel写一个demo。既然前面说了ViewModel存在的时间是第一次创建到页面完全销毁。那么我们就以屏幕旋转的场景为例。
在Activity中使用
既然我们需要验证ViewModel是否真的可以在屏幕旋转的时候存储数据,那么我们就以计时器为例,先不使用ViewModel,看看结果如何,接下来我们上代码,首先写一个简单的计时器的Demo:
1 | /** |
可以看到,我们在页面OnResume的时候开始计时,在页面Stop的时候停止计时。然后我们在activity中绑定下这个观察者,我们运行下,验证下效果:
上图可见,当我们在屏幕旋转的时候,因为页面的生命周期重新执行了,导致了计时器的数据也被重新初始化了。那么我们怎么用ViewModel解决这个问题呢?首先我们先写一个ViewModel。代码如下:
1 | class MyViewModel(var count: Int = 0) : ViewModel() |
然后我们在Activity中把当前的ViewModel定义一下,代码如下:
1 | private val viewModel by lazy { ViewModelProvider(this).get(MyViewModel::class.java) } |
然后我们给TimeCounter新增一个入参,在添加观察者时把当前的ViewModel传进去即可。代码如下:
1 | lifecycle.addObserver(TimeCounter(viewModel)) |
把原先的count修改为viewModel.count即可。那么现在我们在看一下效果:
哎,可以了。接下来我们需要做的是,在不同场景我们可能需要不同的入参。那如何向ViewModel内部传参呢。别急,ViewModel向我们提供了一个Factory。我们可以通过这个工厂类来解决上述问题。直接上代码:
1 | class MyViewModelFactory(var count: Int) : ViewModelProvider.Factory { |
既然定义了这个工厂类,那我们怎么使用呢?不慌,ViewModelProvider也提供了Factory相关参数,只需要把定义修改成如下代码:
1 | private val viewModel by lazy { ViewModelProvider(this, MyViewModelFactory(10)).get(MyViewModel::class.java) } |
这样就可以解决了初始值的问题了。我们先来看一下效果:
哎,解决了。不过,等等,我直接new一个ViewModel把初始值传进去不就行了吗?为什么还要写一个工厂类,搞这么麻烦干嘛。但是如果通过new ViewModel的方法进行传值的话,它就与Activity的生命周期绑定了,所以,切记,不要使用新建传值的方法去定义初始值。
在Fragment中使用
在日常开发中,Activity和Fragment通信是一个很常见的问题,需要通过定义相关接口去处理。此外,这两个Fragment都必须处理另一个Fragment尚未创建或不可见的情况。那么我们通过共享Activity的ViewModel来解决上述问题。那么接下来,我们依旧直接上代码,我们只需要在Fragment中这样定义ViewModel就可以了:
1 | private val viewModel by lazy { activity?.let { ViewModelProvider(this).get(MyViewModel::class.java) } } |
需要注意的是,我们定义的时候需要传入的是Activity的上下文,而不是Fragment的。
此方法具有以下优势:
- Activity不需要执行任何操作,也不需要对此通信有任何了解。
- 除了ViewModel约定之外,Fragment不需要相互了解。如果其中一个Fragment消失,另一个Fragment将继续照常工作。
- 每个Fragment都有自己的生命周期,而不受另一个Fragment的生命周期的影响。如果一个Fragment 替换另一个Fragment,界面将继续工作而没有任何问题。
ViwModel实现原理
既然需要看ViewModel的原理,我们回过头去看下ViewModel生命周期的那张图,可以看出Activity完全销毁后才调用了ViewModel的onCleared方法。那我们使用反推法,看看ViewModel的onCleared方法是何时调用的,先上代码:
1 | public abstract class ViewModel { |
可以看到在ViewModel的clear方法中调用了onCleared方法,那么我们看看clear方法是在哪里调用的。
1 | public class ViewModelStore { |
可以看到是在ViewModelStore的clear方法里面调用的,那么继续向上追踪,可以发现在ComponentActivity中调用了,具体代码如下:
1 | getLifecycle().addObserver(new LifecycleEventObserver() { |
可以看到当activity调用了OnDestory并且isChangingConfigurations不成立的时候,会去调用ViewModelStore的clear方法。那我们就知道了为什么单单调用了onDestory,ViewModel的实例还存在的原因。那么我们看下isChangingConfigurations这个方法是用来干嘛的。
由源码可知,如果在onStop()中发现isChangingConfigurations()的返回值为false,则说明该Activity被暂停了,暂时不需要使用该资源了,则可以释放引用的资源;如果isChangingConfigurations()返回值为true,则说明该Activity正在被销毁然后重新创建一个新的,这种情况下引用的资源还需要马上用到(在新创建的Activity中),这样可以先不释放该资源,当新的Activity创建好后,则可以立即使用该资源。
我们使用反推法证实了在Activity完全结束后ViewModel的销毁才会执行。那么ViewModel的创建呢?话不多说,我们直接看get方法:
1 |
|
可以看出它先从ViewModelStore获取ViewModel实例。如果获取到了就直接返回。如果未获取到就直接通过工厂类创建一个,然后放入mViewModelStore中去。所以我们知道了,即使Activity重新创建了,因为ViewModel没有销毁,所以之前存储在ViewModel的数据源还在。这就合理的解释了,为什么ViewModel可以解决屏幕旋转后页面数据存储的问题。
总结
本文主要介绍了ViewModel的使用以及原理,小结下,ViewModel在Activity首次onCreate的时候创建,并存入ViewModelStore,后续就算多次调用了onCreate方法,它永远都是读取上次在ViewModelStore存入的ViewModel实例。在Activity完全销毁后,调用ViewModel的onCleared方法将其清除。