1f02d55b7a4204d13f290212175a26ea
使用Kotlin构建MVVM应用程序—提高篇:ViewModel

写在前面

大家好,这里是使用Kotlin构建MVVM应用程序—提高篇:ViewModel。

本篇文章将介绍google推荐的架构组件ViewModel的使用方法及实现原理。

为什么要有ViewModel?

为什么?看到ViewModel这个名字相信都会联系到MVVM架构中的VM。

但是在我看来,这两者并非是一个意思。如果你想实现MVVM架构的APP,按照

使用Kotlin构建MVVM应用程序基础篇的内容就已经足够了。

而我推测google把它称为ViewModel的原因可能有两点:

  1. ViewModel架构组件是为VM层服务的。
  2. 容易联想到MVVM架构,代表着google更推荐Android工程师们应用MVVM架构,而并非冗杂繁复的MVP。

当然这些是题外话。既然不使用ViewModel也能构建MVVM应用,那么ViewModel是来做什么的呢?

The ViewModel class is designed to store and manage UI-related data in a lifecycle conscious way. The ViewModel class allows data to survive configuration changes such as screen rotations.

简单说来,ViewModel是用来存储和管理UI相关的数据,将一个Activity或Fragment组件相关的数据逻辑抽象出来,并能适配组件的生命周期,如当屏幕旋转Activity重建后,ViewModel中的数据依然有效。它还可以帮助开发者轻易实现 Fragment 与 Fragment 之间, Activity 与 Fragment 之间的通讯以及共享数据

其中最具有吸引力的功能就是屏幕旋转Activity重建后,ViewModel中的数据依然有效。遥想当年,屏幕旋转当真是开发者不得不迈过的槛。而现在很多应用都只要求竖屏或者强制竖屏,不得横屏,所以对于这样的应用,我的建议是可以不用ViewModel组件,按照普通的VM开发就可以了。当然适当了解一下还是可以的。

ViewModel快速开始

首先我们需要在app/build.gradle加入相应的依赖

//ViewModel
implementation "android.arch.lifecycle:extensions:1.1.1"
implementation "android.arch.lifecycle:viewmodel:1.1.1"
kapt "android.arch.lifecycle:compiler:1.1.1"

然后,让你的VM层继承ViewModel组件提供的ViewModel类,如果需要用到Applicationcontext,那么就继承AndroidViewModel类。

class PaoViewModel constructor(private val repo: PaoRepo) :ViewModel(){
    //...
}

在View层的PaoActivity之中,以前的mViewModel是通过Dagger2注入的,而现在需要进行一下修改

class PaoActivity : RxAppCompatActivity() {

    lateinit var mBinding : PaoActivityBinding

    lateinit var mViewModel : PaoViewModel

   
    lateinit var factory: ViewModelProvider.Factory

     override fun onCreate(savedInstanceState: Bundle?) {
        //....
        mViewModel=ViewModelProviders.of(this,factory).get(PaoViewModel::class.java)
        //...
     }

}

这里有两个概念ViewModelProvider.FactoryViewModelProviders。再继续之前,我们需要先了解如何打造自己的ViewModelProvider.Factory

ViewModelProvider.Factory

看到这个名字,自然就联想到工厂模式,这里提供我们需要的mViewModel实例。当然ViewModel没有Dagger2那么神奇,不会帮我们自动生成,所以需要我们自己来实现需要的ViewModelProvider.Factory

class PaoViewModelFactory : ViewModelProvider.Factory{
    //需要实现create方法,返回具体的viewmodel
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        //return T
    }
}

那怎么创建具体的实例呢?可以在这里一个个new出相应的依赖,但既然已经有Dagger2这么棒的依赖管理工具,当然是使用Dagger2了。


class PaoViewModelFactory constructor(private val viewModel:PaoViewModel): ViewModelProvider.Factory{
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (modelClass.isInstance(viewModel)){
            return viewModel as T
        }else{
            throw IllegalArgumentException("unknown model class $modelClass")
        }
    }

}

到此,当然还没有结束。这样的写法只针对单个ViewModel,如果有多个呢?我们自然不希望出现如下的代码


class PaoViewModelFactory constructor(private val viewModel:PaoViewModel,private val viewModelOne:OneViewModel,private val viewModelOther:OtherViewModel): ViewModelProvider.Factory{
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return when{
            modelClass.isInstance(viewModel) -> viewModel as T
            modelClass.isInstance(viewModelOne) -> viewModelOne as T
            modelClass.isInstance(viewModelOther) -> viewModelOther as T
            //...
            else -> throw IllegalArgumentException("unknown model class $modelClass")
        }
    }

}

这样费力不太好并且没有效率的事当然不是有(想)效(偷)率(懒)的程序员喜欢做的事。

简单的做法是注入一个map,value是我们需要的ViewModel实例,key值为相应的Class。

幸运的是通过Dagger2的@IntoMap可以为我们自动构造所需的map对象,是不是对Dagger2开始爱不释手了^_^


abstract class ViewModelModule{

   
   
   (PaoViewModel::class)//自定义的mapKey
    abstract fun bindPaoViewModel(viewModel: PaoViewModel):ViewModel

//   
//   
//   (OtherViewModel::class)
//    abstract fun bindOtherViewModel(viewModel: OtherViewModel):ViewModel 
//    ...

   
    abstract fun bindViewModelFactory(factory:PaoViewModelFactory):ViewModelProvider.Factory
}

然后它放到AppComponent中

(modules = arrayOf(
        AndroidInjectionModule::class,
        AppModule::class,
        ViewModelModule::class,
        ActivityModule::class)
)
interface AppComponent {}

最后修改PaoViewModelFactory,代码参考自android-architecture-components/GitHubBrowserSample


class PaoViewModelFactory constructor(private val creators:Map<Class<out ViewModel> Provider<ViewModel>>): ViewModelProvider.Factory{
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        val creator = creators[modelClass]?:creators.entries.firstOrNull{
            modelClass.isAssignableFrom(it.key)
        }?.value?:throw IllegalArgumentException("unknown model class $modelClass")
        try {
           ("UNCHECKED_CAST")
            return creator.get() as T
        } catch (e: Exception) {
            throw RuntimeException(e)
        }
    }

}

大功告成,现在我们的ViewModel已经具备了屏幕旋转Activity重建后,ViewModel中的数据依然有效的能力。

rotate

原理剖析

首先我们先来看看ViewModel

public abstract class ViewModel {
    /**
     * This method will be called when this ViewModel is no longer used and will be destroyed.
     * <p>
     * It is useful when ViewModel observes some data and you need to clear this subscription to
     * prevent a leak of this ViewModel.
     */
    ("WeakerAccess")
    protected void onCleared() {
    }
}

非常简单,所以不可能是它变的魔术。

和分析Dagger-Android一样,我们继续从入口处断点调试,看看到底是怎么发生的。

  • ViewModelProviders.of(this,factory)

ViewModelProviders

这里主要是构建了一个ViewModelProvider对象

  • ViewModelStores.of(activity)

ViewModelStores

这个ViewModelStore是什么呢?

/**
 * Class to store  ViewModels}.
 *  android.arch.lifecycle.ViewModelStores} provides a  ViewModelStore} for
 * activities and fragments.
 */
public class ViewModelStore {

    private final HashMap<String, ViewModel> mMap = new HashMap<>();

    final void put(String key, ViewModel viewModel) {
        ViewModel oldViewModel = mMap.put(key, viewModel);
        if (oldViewModel != null) {
            oldViewModel.onCleared();
        }
    }

    final ViewModel get(String key) {
        return mMap.get(key);
    }

    /**
     *  Clears internal storage and notifies ViewModels that they are no longer used.
     */
    public final void clear() {
        for (ViewModel vm : mMap.values()) {
            vm.onCleared();
        }
        mMap.clear();
    }
}

很简单,就是一个用来贮存ViewModel实例的类,到这里大家大概也发觉了,其实也是一个map.get(key)的过程,和Dagger-Android一样,key值是对应ViewModel的class。

mViewModel=ViewModelProviders.of(this,factory).get(PaoViewModel::class.java)

当然,仅此还不足以实现保留ViewModel数据的功能,注意holderFragmentFor(activity)

  • holderFragmentFor(activity)
/**
 *
 */
(RestrictTo.Scope.LIBRARY_GROUP)
public static HolderFragment holderFragmentFor(FragmentActivity activity) {
    return sHolderFragmentManager.holderFragmentFor(activity);
}

看来magic就在于这个HolderFragment了,不得不感叹Fragment的强大,虽然日常开发中不怎么好用,但是做一些小事情简直就是利器,再来看看HolderFragment做了什么。

  • HolderFragment
public class HolderFragment extends Fragment implements ViewModelStoreOwner {
    private static final String LOG_TAG = "ViewModelStores";
    //....
    private ViewModelStore mViewModelStore = new ViewModelStore();

    public HolderFragment() {
        setRetainInstance(true);
    }
    //...
      
    public void onDestroy() {
        super.onDestroy();
        mViewModelStore.clear();
    }

    
    
    public ViewModelStore getViewModelStore() {
        return mViewModelStore;
    }
    //...
}

//A scope that owns  ViewModelStore}.
public interface ViewModelStoreOwner {
    /**
     * Returns owned  ViewModelStore}
     *
     * a  ViewModelStore}
     */
    
    ViewModelStore getViewModelStore();
}

HolderFragment实现了ViewModelStoreOwner接口,并且提供了一个ViewModelStore对象,就如前文所说的那样,ViewModelStore贮存了我们所需的ViewModel。那它是怎么保证不重复创建,转屏时ViewModel中的数据依然可用呢?

熟悉Fragment的朋友相信已经明了了。

 public HolderFragment() {
        setRetainInstance(true);
}

通过设置 setRetainInstance(true)。默认为false,设置为true后,屏幕旋转的时候Fragment不会重复创建,它里面的数据自然就还是以前的数据。

  • .get(PaoViewModel::class.java)

getByClass

正如所说,不过这里的key加上了一个默认的名为DEFAULT_KEY的前缀。

private static final String DEFAULT_KEY =
        "android.arch.lifecycle.ViewModelProvider.DefaultKey";
  • get(key,modelClass)
public <T extends ViewModel> T get( String key,  Class<T> modelClass) {
    ViewModel viewModel = mViewModelStore.get(key);

    if (modelClass.isInstance(viewModel)) {
        //noinspection unchecked
        return (T) viewModel;
    } else {
        //noinspection StatementWithEmptyBody
        if (viewModel != null) {
            // TODO: log a warning.
        }
    }

    viewModel = mFactory.create(modelClass);
    mViewModelStore.put(key, viewModel);
    //noinspection unchecked
    return (T) viewModel;
}

通过map和key获取到相应的ViewModel,存在则返回,否则添加到ViewModelStore中。
ViewModeStore

到此,我们的原理就已经剖析完成了。

至于为什么ViewModel组件可以帮助开发者轻易实现 Fragment 与 Fragment 之间, Activity 与 Fragment 之间的通讯以及共享数据?那是当然的啊,毕竟activity还有key值是一样的嘛。

ViewModel生命周期

lifecycle

通过上文的剖析之后,相信对这张图背后的东西有了更深的理解。

HolderFragmentonDestroy()方法被调用的时候

public void onDestroy() {
    super.onDestroy();
    mViewModelStore.clear();
}

再看看 mViewModelStore.clear()

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

最后调用ViewModelonCleared()方法。

写在最后

ViewModel组件理解起来也不困难,主要是利用了Fragment的setRetainInstance(true)方法保证数据不被销毁和重复创建,和Dagger2结合起来更是事半功倍。

github:https://github.com/ditclear/MVVM-Android/tree/viewmodel

参考资料:

google/viewmodel

Android架构组件——ViewModel(推荐)

© 著作权归作者所有
这个作品真棒,我要支持一下!
旨在介绍如何使用Kotlin构建MVVM应用程序,你将会了解到最前沿的开发模式,编写android代码从未如此简单...
0条评论
top Created with Sketch.