Android官方架构组件ViewModel:从前世此生到追本溯源

概述

2017年的Google I/O大会上,Google推出了一系列譬如 Lifecycle、ViewModel、LiveData等一系列 更适合用于MVVM模式开发 的架构组件。php

本文的主角就是 ViewModel ,也许有朋友会提出质疑:前端

ViewModel 这么简单的东西,从API的使用到源码分析,相关内容都烂大街了,你这篇文章还能翻出什么花来?java

我没法反驳,事实上,阅读本文的您可能对MVVM的代码已经 得心应手,甚至是经历了完整项目的洗礼,但我依然想作一次大胆地写做尝试—— 即便对于MVVM模式的思想噗之以鼻,或者已经熟练使用MVVM,本文也尽可能让您有所收获,至少阅读体验不那么枯燥android

ViewModel的前世此生

ViewModel,或者说 MVVM (Model-View-ViewModel),并不是是一个新鲜的词汇,它的定义最先起源于前端,表明着 数据驱动视图 的思想。git

好比说,咱们能够经过一个String类型的状态来表示一个TextView,同理,咱们也能够经过一个List<T>类型的状态来维护一个RecyclerView的列表——在实际开发中咱们经过观察这些数据的状态,来维护UI的自动更新,这就是 数据驱动视图(观察者模式)github

每当String的数据状态发生变动,View层就能检测并自动执行UI的更新,同理,每当列表的数据源List<T>发生变动,RecyclerView也会自动刷新列表:编程

对于开发者来说,在开发过程当中能够大幅减小UI层和Model层相互调用的代码,转而将更多的重心投入到业务代码的编写数组

ViewModel 的概念就是这样被提出来的,我对它的形容相似一个 状态存储器 , 它存储着UI中各类各样的状态, 以 登陆界面 为例,咱们很容易想到最简单的两种状态 :架构

class LoginViewModel {
    val username: String  // 用户名输入框中的内容
    val password: String  // 密码输入框中的内容
}
复制代码

先不纠结于代码的细节,如今咱们知道了ViewModel的重心是对 数据状态的维护。接下来咱们来看看,在17年以前Google尚未推出ViewModel组件以前,Android领域内MVVM 百花齐放的各类形态 吧。app

1.群雄割据时代的百花齐放

说到MVVM就不得不提Google在2015年IO大会上提出的DataBinding库,它的发布直接促进了MVVM在Android领域的发展,开发者能够直接经过将数据状态经过 伪Java代码 的形式绑定在xml布局文件中,从而将MVVM模式的开发流程造成一个 闭环

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
       <data>
           <variable name="user" type="User" />
       </data>
      <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{ user.name }" android:textSize="20sp" />
</layout>
复制代码

经过 伪Java代码 将UI的逻辑直接粗暴的添加进xml布局文件中达到和View的绑定,DataBinding这种实现方式引发了 强烈的争论。直至现在,依然有不少开发者没法接受DataBinding,这是彻底能够理解的,由于它确实 很难定位语法的错误和运行时的崩溃缘由

MVVM模式并不必定依赖于DataBinding,可是除了DataBinding,开发者当时并无足够多的选择——直至目前,仍然有部分的MVVM开发者坚持不使用 DataBinding,取而代之使用生态圈极为丰富的RxJava(或者其余)代替 DataBinding的数据绑定。

若是说当时对于 数据绑定 的库至少还有官方的DataBinding可供参考,ViewModel的规范化则是很是困难——基于ViewModel层进行状态的管理这个基本的约束,不一样的项目、不一样的依赖库加上不一样的开发者,最终代码中对于 状态管理 的实现方式都有很大的不一样。

好比,有的开发者,将 ViewModel 层像 MVP 同样定义为一个接口:

interface IViewModel 

open class BaseViewModel: IViewModel
复制代码

也有开发者(好比这个repo)直接将ViewModel层继承了可观察的属性(好比dataBinding库的BaseObservable),并持有Context的引用:

public class CommentViewModel extends BaseObservable {

    @BindingAdapter("containerMargin")
    public static void setContainerMargin(View view, boolean isTopLevelComment) {
        //...
    }
}
复制代码

一千我的有一千个哈姆雷特,不一样的MVVM也有大相径庭的实现方式,这种百花齐放的代码风格、难以严格统一的 开发流派 致使代码质量的良莠不齐,代码的可读性更是天差地别。

再加上DataBinding自己致使代码阅读性的下降,真可谓南门北派华山论剑,各类思想喷涌而出——从思想的碰撞交流来说,这并不是坏事,可是对于当时想学习MVVM的我来说,实在是看得眼花缭乱,在学习接触的过程当中,我也不可避免的走了许多弯路。

2.Google对于ViewModel的规范化尝试

咱们都知道Google在去年的 I/O 大会很是隆重地推出了一系列的 架构组件ViewModel正是其中之一,也是本文的主角。

有趣的是,相比较于惹眼的 LifecycleLiveDataViewModel 显得很是低调,它主要提供了这些特性:

  • 配置更改期间自动保留其数据 (好比屏幕的横竖旋转)
  • ActivityFragment等UI组件之间的通讯

若是让我直接吹捧ViewModel多么多么优秀,我会很是犯难,由于它表面展示的这些功能实在不够惹眼,可是有幸截止目前为止,我花费了一些笔墨阐述了ViewModel在这以前的故事——它们是接下来正文不可缺乏的铺垫

3.ViewModel在这以前的窘境

也许您还没有意识到,在官方的ViewModel发布以前,MVVM开发模式中,ViewModel层的一些窘境,但实际上我已经尽力经过叙述的方式将这些问题描述出来:

3.1 更规范化的抽象接口

在官方的ViewModel发布以前,ViewModel层的基类多种多样,内部的依赖和公共逻辑更是五花八门。新的ViewModel组件直接对ViewModel层进行了标准化的规范,即便用ViewModel(或者其子类AndroidViewModel)。

同时,Google官方建议ViewModel尽可能保证 纯的业务代码,不要持有任何View层(Activity或者Fragment)或Lifecycle的引用,这样保证了ViewModel内部代码的可测试性,避免由于Context等相关的引用致使测试代码的难以编写(好比,MVP中Presenter层代码的测试就须要额外成本,好比依赖注入或者Mock,以保证单元测试的进行)。

3.2 更便于保存数据

由系统响应用户交互或者重建组件,用户没法操控。当组件被销毁并重建后,原来组件相关的数据也会丢失——最简单的例子就是屏幕的旋转,若是数据类型比较简单,同时数据量也不大,能够经过onSaveInstanceState()存储数据,组件重建以后经过onCreate(),从中读取Bundle恢复数据。但若是是大量数据,不方便序列化及反序列化,则上述方法将不适用。

ViewModel的扩展类则会在这种状况下自动保留其数据,若是Activity被从新建立了,它会收到被以前相同ViewModel实例。当所属Activity终止后,框架调用ViewModelonCleared()方法释放对应资源:

这样看来,ViewModel是有必定的 做用域 的,它不会在指定的做用域内生成更多的实例,从而节省了更多关于 状态维护(数据的存储、序列化和反序列化)的代码。

ViewModel在对应的 做用域 内保持生命周期内的 局部单例,这就引起一个更好用的特性,那就是FragmentActivity等UI组件间的通讯。

3.3 更方便UI组件之间的通讯

一个Activity中的多个Fragment相互通信是很常见的,若是ViewModel的实例化做用域为Activity的生命周期,则两个Fragment能够持有同一个ViewModel的实例,这也就意味着数据状态的共享:

public class AFragment extends Fragment {
    private CommonViewModel model;
    public void onActivityCreated() {
        model = ViewModelProviders.of(getActivity()).get(CommonViewModel.class);
    }
}

public class BFragment extends Fragment {
    private CommonViewModel model;
    public void onActivityCreated() {
        model = ViewModelProviders.of(getActivity()).get(CommonViewModel.class);
    }
}
复制代码

上面两个Fragment getActivity()返回的是同一个宿主Activity,所以两个Fragment之间返回的是同一个ViewModel

我不知道正在阅读本文的您,有没有冒出这样一个想法:

ViewModel提供的这些特性,为何感受互相之间没有联系呢?

这就引起下面这个问题,那就是:

这些特性的本质是什么?

4. ViewModel:对状态的持有和维护

ViewModel层的根本职责,就是负责维护UI的状态,追根究底就是维护对应的数据——毕竟,不管是MVP仍是MVVM,UI的展现就是对数据的渲染。

  • 1.定义了ViewModel的基类,并建议经过持有LiveData维护保存数据的状态;
  • 2.ViewModel不会随着Activity的屏幕旋转而销毁,减小了维护状态的代码成本(数据的存储和读取、序列化和反序列化);
  • 3.在对应的做用域内,保正只生产出对应的惟一实例,多个Fragment维护相同的数据状态,极大减小了UI组件之间的数据传递的代码成本。

如今咱们对于ViewModel的职责和思想都有了必定的了解,按理说接下来咱们应该阐述如何使用ViewModel了,但我想先等等,由于我以为相比API的使用,掌握其本质的思想会让你在接下来的代码实践中如鱼得水

不,不是源码解析...

经过库提供的API接口做为开始,阅读其内部的源码,这是标准掌握代码内部原理的思路,这种方式的时间成本极高,即便有相关源码分析的博客进行引导,文章中大片大片的源码和注释也足以让人望而却步,因而我理所固然这么想

先学会怎么用,再抽空系统学习它的原理和思想吧......

发现没有,这和上学时候的学习方式居然截然相反,甚至说本末倒置也不奇怪——任何一个物理或者数学公式,在使用它作题以前,对它背后的基础理论都应该是优先去系统性学习掌握的(好比,数学公式的学习通常都须要先经过必定方式推导和证实),这样我才能拿着这个知识点对课后的习题触类旁通。这就比如,若是一个老师直接告诉你一个公式,而后啥都不说让你作题,这个老师必定是不合格的。

我也不是很喜欢大篇幅地复制源码,我准备换个角度,站在Google工程师的角度看看怎么样设计出一个ViewModel

站在更高的视角,设计ViewModel

如今咱们是Google工程师,让咱们再回顾一下ViewModel应起到的做用:

  • 1.规范化了ViewModel的基类;
  • 2.ViewModel不会随着Activity的屏幕旋转而销毁;
  • 3.在对应的做用域内,保正只生产出对应的惟一实例,保证UI组件间的通讯。

1.设计基类

这个简直太简单了:

public abstract class ViewModel {

    protected void onCleared() {
    }
}
复制代码

咱们定义一个抽象的ViewModel基类,并定义一个onCleared()方法以便于释放对应的资源,接下来,开发者只须要让他的XXXViewModel继承这个抽象的ViewModel基类便可。

2.保证数据不随屏幕旋转而销毁

这是一个很神奇的功能,但它的实现方式却很是简单,咱们先了解这样一个知识点:

setRetainInstance(boolean)Fragment中的一个方法。将这个方法设置为true就可使当前FragmentActivity重建时存活下来

这彷佛和咱们的功能很是吻合,因而咱们不由这样想,可不可让Activity持有这样一个不可见的Fragment(咱们干脆叫他HolderFragment),并让这个HolderFragment调用setRetainInstance(boolean)方法并持有ViewModel——这样当Activity由于屏幕的旋转销毁并重建时,该Fragment存储的ViewModel天然不会被随之销毁回收了:

public class HolderFragment extends Fragment {

     public HolderFragment() { setRetainInstance(true); }
    
      private ViewModel mViewModel;
      // getter、setter...
}
复制代码

固然,考虑到一个复杂的UI组件可能会持有多个ViewModel,咱们更应该让这个不可见的HolderFragment持有一个ViewModel的数组(或者Map)——咱们干脆封装一个叫ViewModelStore的容器对象,用来承载和代理全部ViewModel的管理:

public class ViewModelStore {
    private final HashMap<String, ViewModel> mMap = new HashMap<>();
    // put(), get(), clear()....
}

public class HolderFragment extends Fragment {

      public HolderFragment() { setRetainInstance(true); }

      private ViewModelStore mViewModelStore = new ViewModelStore();
}
复制代码

好了,接下来须要作的就是,在实例化ViewModel的时候:

1.当前Activity若是没有持有HolderFragment,就实例化并持有一个HolderFragment 2.Activity获取到HolderFragment,并让HolderFragmentViewModel存进HashMap中。

这样,具备生命周期的Activity在旋转屏幕销毁重建时,由于不可见的HolderFragment中的ViewModelStore容器持有了ViewModelViewModel和其内部的状态并无被回收销毁。

这须要一个条件,在实例化ViewModel的时候,咱们彷佛还须要一个Activity的引用,这样才能保证 获取或者实例化内部的HolderFragment并将ViewModel进行存储

因而咱们设计了这样一个的API,在ViewModel的实例化时,加入所需的Activity依赖:

CommonViewModel viewModel = ViewModelProviders.of(activity).get(CommonViewModel.class)
复制代码

咱们注入了Activity,所以HolderFragment的实例化就交给内部的代码执行:

HolderFragment holderFragmentFor(FragmentActivity activity) {
     FragmentManager fm = activity.getSupportFragmentManager();
     HolderFragment holder = findHolderFragment(fm);
     if (holder != null) {
          return holder;
      }
      holder = createHolderFragment(fm);
      return holder;
}
复制代码

这以后,由于咱们传入了一个ViewModelClass对象,咱们默认就能够经过反射的方式实例化对应的ViewModel,并交给HolderFragment中的ViewModelStore容器存起来:

public <T extends ViewModel> T get(Class<T> modelClass) {
      // 经过反射的方式实例化ViewModel,并存储进ViewModelStore
      viewModel = modelClass.getConstructor(Application.class).newInstance(mApplication);
      mViewModelStore.put(key, viewModel);
      return (T) viewModel;
 }
复制代码

3.在对应的做用域内,保正只生产出对应的惟一实例

如何保证在不一样的Fragment中,经过如下代码生成同一个ViewModel的实例呢?

public class AFragment extends Fragment {
    private CommonViewModel model;
    public void onActivityCreated() {
        model = ViewModelProviders.of(getActivity()).get(CommonViewModel.class);
    }
}

public class BFragment extends Fragment {
    private CommonViewModel model;
    public void onActivityCreated() {
        model = ViewModelProviders.of(getActivity()).get(CommonViewModel.class);
    }
}
复制代码

其实很简单,只须要在上一步实例化ViewModelget()方法中加一个判断就好了:

public <T extends ViewModel> T get(Class<T> modelClass) {
      // 先从ViewModelStore容器中去找是否存在ViewModel的实例
      ViewModel viewModel = mViewModelStore.get(key);
     
      // 若ViewModel已经存在,就直接返回
      if (modelClass.isInstance(viewModel)) {
            return (T) viewModel;
      }
       
      // 若不存在,再经过反射的方式实例化ViewModel,并存储进ViewModelStore
      viewModel = modelClass.getConstructor(Application.class).newInstance(mApplication);
      mViewModelStore.put(key, viewModel);
      return (T) viewModel;
 }
复制代码

如今,咱们成功实现了预期的功能——事实上,上文中的代码正是ViewModel官方核心部分功能的源码,甚至默认ViewModel实例化的API也没有任何改变:

CommonViewModel viewModel = ViewModelProviders.of(activity).get(CommonViewModel.class);
复制代码

固然,由于篇幅所限,我将源码进行了简单的删减,同时没有讲述构造方法中带参数的ViewModel的实例化方式,但对于目前已经掌握了设计思想原理的你,学习这些API的使用几乎不费吹灰之力。

总结与思考

ViewModel是一个设计很是精巧的组件,它功能并不复杂,相反,它简单的难以置信,你甚至只须要了解实例化ViewModel的API如何调用就好了。

同时,它的背后掺杂的思想和理念是值得去反复揣度的。好比,如何保证对状态的规范化管理?如何将纯粹的业务代码经过良好的设计下沉到ViewModel中?对于很是复杂的界面,如何将各类各样的功能抽象为数据状态进行解耦和复用?随着MVVM开发的深刻化,这些问题都会一个个浮出水面,这时候ViewModel组件良好的设计和这些不起眼的小特性就随时有可能成为璀璨夺目的闪光点,帮你攻城拔寨。

--------------------------广告分割线------------------------------

系列文章

争取打造 Android Jetpack 讲解的最好的博客系列

Android Jetpack 实战篇


关于我

Hello,我是却把清梅嗅,若是您以为文章对您有价值,欢迎 ❤️,也欢迎关注个人我的博客或者Github

若是您以为文章还差了那么点东西,也请经过关注督促我写出更好的文章——万一哪天我进步了呢?

相关文章
相关标签/搜索