Android Jetpack架构组件——一文带你了解ViewModel的使用及原理

Posted 我就是马云飞

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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:

/**
 * @date:2021/2/22
 * @author:Silence
 * @describe:
 **/
class TimeCounter : LifecycleObserver 

    private var canCount = false
    private var count = 0

    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    private fun start() 
        canCount = true
        Thread 
            while (canCount) 
                Log.i(TAG, "start: $count")
                Thread.sleep(1000)
                count++
            
        .start()
    

    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    private fun stop() 
        canCount = false
        Log.i(TAG, "stop: ")
    

    companion object 
        private const val TAG = "TimeCounter"
    

可以看到,我们在页面OnResume的时候开始计时,在页面Stop的时候停止计时。然后我们在activity中绑定下这个观察者,我们运行下,验证下效果:

上图可见,当我们在屏幕旋转的时候,因为页面的生命周期重新执行了,导致了计时器的数据也被重新初始化了。那么我们怎么用ViewModel解决这个问题呢?首先我们先写一个ViewModel。代码如下:

class MyViewModel(var count: Int = 0) : ViewModel()

然后我们在Activity中把当前的ViewModel定义一下,代码如下:

 private val viewModel by lazy  ViewModelProvider(this).get(MyViewModel::class.java) 

然后我们给TimeCounter新增一个入参,在添加观察者时把当前的ViewModel传进去即可。代码如下:

    lifecycle.addObserver(TimeCounter(viewModel))

把原先的count修改为viewModel.count即可。那么现在我们在看一下效果:

哎,可以了。接下来我们需要做的是,在不同场景我们可能需要不同的入参。那如何向ViewModel内部传参呢。别急,ViewModel向我们提供了一个Factory。我们可以通过这个工厂类来解决上述问题。直接上代码:

class MyViewModelFactory(var count: Int) : ViewModelProvider.Factory 
    override fun <T : ViewModel?> create(modelClass: Class<T>): T 
        return MyViewModel(count) as T
    

既然定义了这个工厂类,那我们怎么使用呢?不慌,ViewModelProvider也提供了Factory相关参数,只需要把定义修改成如下代码:

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就可以了:

  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方法是何时调用的,先上代码:

public abstract class ViewModel 
   
    @Nullable
    private final Map<String, Object> mBagOfTags = new HashMap<>();
    private volatile boolean mCleared = false;

    @SuppressWarnings("WeakerAccess")
    protected void onCleared() 
    

    @MainThread
    final void clear() 
        mCleared = true;
        if (mBagOfTags != null) 
            synchronized (mBagOfTags) 
                for (Object value : mBagOfTags.values()) 
                    closeWithRuntimeException(value);
                
            
        
        onCleared();
    
    
    ...

可以看到在ViewModel的clear方法中调用了onCleared方法,那么我们看看clear方法是在哪里调用的。

public class ViewModelStore 

    ...
    
    public final void clear() 
        for (ViewModel vm : mMap.values()) 
            vm.clear();
        
        mMap.clear();
    

可以看到是在ViewModelStore的clear方法里面调用的,那么继续向上追踪,可以发现在ComponentActivity中调用了,具体代码如下:

 getLifecycle().addObserver(new LifecycleEventObserver() 
            @Override
            public void onStateChanged(@NonNull LifecycleOwner source,
                    @NonNull Lifecycle.Event event) 
                if (event == Lifecycle.Event.ON_DESTROY) 
                    if (!isChangingConfigurations()) 
                        getViewModelStore().clear();
                    
                
            
        );

可以看到当activity调用了OnDestory并且isChangingConfigurations不成立的时候,会去调用ViewModelStore的clear方法。那我们就知道了为什么为什么单单调用了onDestory,ViewModel的实例还存在的原因。那么我们看下isChangingConfigurations这个方法是用来干嘛的。

由源码可知,如果在onStop()中发现isChangingConfigurations()的返回值为false,则说明该Activity被暂停了,暂时不需要使用该资源了,则可以释放引用的资源;如果isChangingConfigurations()返回值为true,则说明该Activity正在被销毁然后重新创建一个新的,这种情况下引用的资源还需要马上用到(在新创建的Activity中),这样可以先不释放该资源,当新的Activity创建好后,则可以立即使用该资源。

我们使用反推法证实了在Activity完全结束后ViewModel的销毁才会执行。那么ViewModel的创建呢?话不多说,我们直接看get方法:

    @NonNull
    @MainThread
    public <T extends ViewModel> T get(@NonNull Class<T> modelClass) 
        String canonicalName = modelClass.getCanonicalName();
        if (canonicalName == null) 
            throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels");
        
        return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
    
    
    @SuppressWarnings("unchecked")
    @NonNull
    @MainThread
    public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) 
        ViewModel viewModel = mViewModelStore.get(key);
        if (modelClass.isInstance(viewModel)) 
            if (mFactory instanceof OnRequeryFactory) 
                ((OnRequeryFactory) mFactory).onRequery(viewModel);
            
            return (T) viewModel;
         else 
            //noinspection StatementWithEmptyBody
            if (viewModel != null) 
                // TODO: log a warning.
            
        
        if (mFactory instanceof KeyedFactory) 
            viewModel = ((KeyedFactory) mFactory).create(key, modelClass);
         else 
            viewModel = mFactory.create(modelClass);
        
        mViewModelStore.put(key, viewModel);
        return (T) viewModel;
    

可以看出它先从ViewModelStore获取ViewModel实例。如果获取到了就直接返回。如果未获取到就直接通过工厂类创建一个,然后放入mViewModelStore中去。所以我们知道了,即使Activity重新创建了,因为ViewModel没有销毁,所以之前存储在ViewModel的数据源还在。这就合理的解释了,为什么ViewModel可以解决屏幕旋转后页面数据存储的问题。

总结

本文主要介绍了ViewModel的使用以及原理,小结下,ViewModel在Activity首次onCreate的时候创建,并存入ViewModelStore,后续就算多次调用了onCreate方法,它永远都是读取上次在ViewModelStore存入的ViewModel实例。在Activity完全销毁后,调用ViewModel的onCleared方法将其清除。

参考

官网

本文首发于我的个人博客:Android Jetpack架构组件——一文带你了解ViewModel的使用及原理
更多文章请关注我的公众号:码农职场

以上是关于Android Jetpack架构组件——一文带你了解ViewModel的使用及原理的主要内容,如果未能解决你的问题,请参考以下文章

Android Jetpack架构组件一文带你了解Lifecycle(原理篇)

Android Jetpack架构组件一文带你了解ViewModel的使用和原理

Android Jetpack架构组件——一文带你了解ViewModel的使用及原理

Android Jetpack架构组件——一文带你了解ViewModel的使用及原理

Android Jetpack架构组件带你了解Android Jetpack

Android Jetpack架构组件从入门到精通,一文通解!