1 MVVM总览

本文包含AndroidMVVM体系中的很多部分,主要对ViewModel+DataBinding+RxJava+LiveData+Lifecycle等笔者所使用的技术体系进行解析.

本文字数较多,内容较为完整并且后续还会追加更新,阅读本篇文章需要较长时间,建议读者分段阅读.

所有文字均为个人学习总结和理解,仅供参考,如有纰漏还请指出,笔者不胜感激.

1.1 配置环境

  • 笔者的Android Studio版本=3.2
  • Jetpack最低兼容到Android=2.1,API=7

1.2 为什么要选择MVVM?

要回答这个问题首先就要介绍MVCMVP这两种模式,从MVCMVVM其实大家想的都是怎么把ModelView尽可能的拆开(熟悉三者定义的朋友可以跳过该节).

1.2.1 MVC

MVCModel-View-Controller)即传统Android开发中最常用的模式:

  • 通常使用Activity/Fragment作为Controller层,
  • android.view.View的子类以xml构建文件构建起的布局作为View
  • SQLite数据库,网络请求作为Model层.

但由于Activity/Fragment的功能过于强大并且实际上包含了部分View层功能,导致最后Activity/Fragment既承担了View的责任,又承担了Controller的责任.所以一般较复杂的页面,Activity/Fragment很容易堆积代码,最终导致Controller混杂了View层和业务逻辑(也就是你们所知道的一个Activity三千行)

MVCView层与Model几乎几乎完全没有隔离,View层可以直接操作Model层,Model层的回调里也可能会直接给View赋值.Controller的概念被弱化,最后只剩下MV没有C了.

这也将导致但你想把某个界面上的元素进行更新时,他会牵扯到一堆跟Model层相关的代码,这个问题在你变更Model层的时候同样也会出现,这个问题其实是没有很好的将逻辑分层导致的.

1.2.2 MVP

MVPModel-View-Presenter)架构设计,是当下最流行的开发模式,目前主要以Google推出的TodoMVP为主,MVP不是一种框架,它实际上更类似一种分层思想,一种接口约定,具体体现在下面:

  • 定义IView接口,并且在接口中约定View层的各种操作,使用android.view.View的子类以xml构建文件构建起的布局Activity/Fragment作为布局控制器,实现IView这个View层的接口,View层的实际实现类保留一个IPresenter接口的实例.
  • 定义IPresenter接口,并且在接口中约定Presenter层的各种操作.可以使用一个与View无关的类实现它,一般是XxxPresenterImpl.通常情况下Presenter层会包含Model层的引用和一个IView接口的引用,但不应该直接或者间接引用Viewandroid.view.View的子类,甚至是操作的参数中也最好不要有android.view.View的子类传进来,因为它应该只负责业务逻辑和数据的处理并通过统一的接口IView传递到View层.
  • 不需要为Model层定义一个IModel的接口,这一层是改造最小的.以前该怎么来现在也差不多该怎么来.但是现在Presenter把它和View隔开了,Presenter就可以作为一段独立的逻辑被复用.

MVP模式解决了MVC中存在的分层问题,Presenter层被突出强调,实际上也就是真正意义上实现了的MVC

但是MVP中其实仍然存在一些问题,比如当业务逻辑变得复杂以后,IPresenterIView层的操作数量可能将会成对的爆炸式增长,新增一个业务逻辑,可能要在两边增加数个通信接口,这种感觉很蠢.

并且,我们要知道一个Presenter是要带一个IView的,当一个Presenter需要被复用时,对应的View就要去实现所有这些操作,但往往一些操作不是必须实现的,这样会留下一堆TODO,很难看.

1.2.3 MVVM

MVVMModel-View-ViewModel)由MVP模式演变而来,它由View层,DataBinding,ViewModel层,Model层构成,是MVP的升级版并由GoogleJetpack工具包提供框架支持:

  • View层包含布局,以及布局生命周期控制器(Activity/Fragment)
  • DataBinding用来实现View层与ViewModel数据的双向绑定(但实际上在Android JetpackDataBinding只存在于布局和布局生命周期控制器之间,当数据变化绑定到布局生命周期控制器时再转发给ViewModel,布局控制器可以持有DataBindingViewModel不应该持有DataBinding)
  • ViewModelPresenter大致相同,都是负责处理数据和实现业务逻辑,但是ViewModel层不应该直接或者间接地持有View层的任何引用,因为一个ViewModel不应该直达自己具体是和哪一个View进行交互的.ViewModel主要的工作就是将Model提供来的数据直接翻译成View层能够直接使用的数据,并将这些数据暴露出去,同时ViewModel也可以发布事件,供View层订阅.
  • Model层与MVP中一致.

MVVM的核心思想是观察者模式,它通过事件和转移View数据持有权来实现View层与ViewModel层的解耦.

MVVMView不是数据的实际持有者,它只负责数据如何呈现以及点击事件的传递,不做的数据处理工作,而数据的处理者和持有者变成ViewModel,它通过接收View层传递过来的时间改变自身状态,发出事件或者改变自己持有的数据触发View的更新.

MVVM解决了MVP中的存在的一些问题,比如它无需定义接口,ViewModelView层彻底无关更好复用,并且有GoogleAndroid Jetpack作为强力后援.

但是MVVM也有自己的缺点,那就是使用MVVM的情况下ViewModelView层的通信变得更加困难了,所以在一些极其简单的页面中请酌情使用,否则就会有一种脱裤子放屁的感觉,在使用MVP这个道理也依然适用.

2 DataBinding

2.1 坑

要用一个框架那么就要先说它的点.那就是不建议在使用DataBinding的模块同时使用apply plugin: 'kotlin-kapt'.

因为现在kapt还有很多Bug,使用kapt时,在WindowsDataBinding格式下的xml中如果包含有中文,会报UTF-8相关的错误.

笔者一开始猜想这是由于JVM启动参数没有设置成-Dfile.encoding=UTF-8导致的,在gradle.properties中改过了,无果,Stack Overflow搜过了,没找到,如果有大佬知道怎么解决,还请指点一二

如果你在模块中同时使用kotlinDataBinding是可以的,但是请一定不要使用kapt,除非JB那帮大佬搞定这些奇怪的问题.

这就意味这你所有的kotlin代码都不能依赖注解处理器来为你的代码提供附加功能,但是你可以把这些代码换成等价的Java实现,它们可以工作得很好.

2.2 DataBinding的兼容性

先说一点,DataBinding风格的xml会有"奇怪"的东西入侵Android原生的xml格式,这种格式LayoutInfalter是无法理解,但是,当你对这些奇怪的xml使用LayoutInfalter#inflate时亦不会报错,并且布局也正常加载了,这是为什么呢?

这是因为在打包时,Gradle通过APT把你的DataBinding风格的xml全部翻译了一遍,让LayoutInfalter能读懂他们,正是因为这个兼容的实现,而使得我们可以在使用和不使用DataBinding间自由的切换.

2.3 DataBinding风格的XML

要想使用DataBinding,先在模块的build.gradle中添加

android{    //省略...    dataBinding {        enabled = true    }}复制代码

来启用DataBinding支持.

DataBinding不需要额外的类库支持,它被附加在你的android插件中,它的版本号与你的android插件版本一致.

classpath 'com.android.tools.build:gradle:3.3.2'复制代码

DataBinding风格的xml中,最外层必须是layout标签,并且不支持merge标签,编写xml就像下面这样

"http://schemas.android.com/apk/res/android"    xmlns:app="http://schemas.android.com/apk/res-auto">            "text"            type="String"/>        "action"            type="android.view.View.OnClickListener"/>        "@{action}"        android:layout_width="match_parent"        android:layout_height="match_parent"/>复制代码

2.3.1 变量领域

data标签包裹的是变量领域,在这里你可以使用variable定义这个布局所要绑定的变量类型,使用name来指定变量名,然后用type来指定其类型.

如果一些类型比较长,而且由需要经常使用你可以像Java一样使用import导入他们(java.lang.*会被默认导入),然后就不用写出完全限定名了,就像这样

        type="android.view.View"            alias="Action"/>        "action"            type="Action"/>复制代码

有必要时(比如名字冲突),你还可以用Action为一个类型指定一个别名,这样你就能在下文中使用这个别名.

2.3.2 转义字符

熟悉xml的同学可能都知道<>xml中是非法字符,那么要使用泛型的时候,我们就需要使用xml中的转义字符<>来进行转义

        //↓错误,编译时会报错×        "list"            type="java.util.List"/>        //↓正确,可以通过编译√        "list"            type="java.util.List<String>"/>复制代码

data标签结束后就是原本的布局编写的位置了,这部分基本和以前差不多,只是加入了DataBinding表达式

            //......        "@{action}"        android:layout_width="match_parent"        android:layout_height="match_parent"/>复制代码

2.3.3 DataBinding表达式

@{}包裹的位置被称为DataBinding表达式,DataBinding表达式几乎支持Java所有的运算符,并且增加了一些额外的操作,这允许我们在xml中有一定的Java编程体验,学过Java web的同学可能会觉得它很像JSP:

  • 不需要xml转义的二元运算+,-,/,*,%,||,|,^,==
  • 需要xml转义的二元运算&&,>> >>>,<<,>,<,>=,<=,与泛型一样运算符>=,>,<,<=等,也是需要转义的,&需要用&转义,这确实有些蹩脚,但这是xml的局限性,我们无法避免,所以在DataBinding风格的xml中应该尽可能的少用这些符号.
  • lambda表达式@{()->persenter.doSomething()}
  • 三元运算?:
  • null合并运算符??,若左边不为空则选择左边,否则选择右边
android:text="@{nullableString??`This a string`}"复制代码
  • 自动导入的context变量,你可以在xml中的任意表达式使用context这个变量,该Context是从该布局的根ViewgetContext获取的,如果你设置了自己的context变量,那么将会覆盖掉它
  • 若表达式中有字符串文本xml需要特殊处理
用单引号包围外围,表达式使用双引号android:text='@{"This a string"}'或者使用`包围字符串,对,就Esc下面那个键的符号android:text="@{`This a string`}"复制代码
  • 判断类型instanceof
  • 括号()
  • 空值null
  • 方法调用,字段访问,以及GetterSetter的简写,比如User#getNameUser#setName现在都可以直接写成@{user.name},这种表达式也是最简单的表达式,属于直接赋值表达式
  • 默认值default,在xml
`android:text="@{file.name, default=`no name`}"`复制代码
  • 下标[],不只是数组,List,SparseArray,Map现在都可以使用该运算符
  • 使用@读取资源文件,如下,但是不支持读取mipmap下的文件
android:text="@{@string/text}"//或者把它作为表达式的一部分android:padding="@{large? @dimen/large : @dimen/small}"复制代码

有一些资源需要显示引用

类型 正常情况 DataBinding表达式引用
String[] @array @stringArray
int[] @array @intArray
TypedArray @array @typedArray
ColorStateList @animator @stateListAnimator
StateListAnimator @color @colorStateList

还有一些操作是DataBinding表达式中没有的,我们无法使用它们:

  • 没有this
  • 没有super
  • 不能创建对象new
  • 不能使用泛型方法的显示调用Collections.emptyList()

编写简单的DataBinding表达式,就像下面这样

            type="android.view.View"/>        "isShow"            type="Boolean"/>        "@{isShow?View.VISIBLE:View.GONE}"        android:text="@{@string/text}"        android:layout_width="match_parent"        android:layout_height="match_parent"/>复制代码

应该避免出现较为复杂的DataBinding表达式,以全部都是直接赋值表达式为佳,数据的处理应该交给布局控制器或者ViewModel来做,布局应该只负责渲染数据.

2.3.4 使用在Java中生成的ViewDataBinding

使用DataBindingAndroid Studio会为每个xml布局生成一个继承自ViewDataBinding的子类型,来帮助我们将xml文件中定义的绑定关系映射到Java中.

比如,如果你有一个R.layout.fragment_main的布局文件,那么他就会为你在当前包下生成一个,FragmentMainBindingViewDataBinding.

Java实化DataBinding风格xml布局与传统方式有所不同.

  • Actvity
    private ActivityHostBinding mBinding;    @Override    protected void onCreate(@Nullable Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_host);    }复制代码
  • 在自定义ViewFragment
    private FragmentMainBinding mBinding;        @Nullable    @Override    public View onCreateView(@NonNull LayoutInflater inflater,                             @Nullable ViewGroup container,                             @Nullable Bundle savedInstanceState) {        mBinding = DataBindingUtil.inflate(inflater,                R.layout.fragment_main,                container,                false);        return mBinding.getRoot();    }复制代码
  • 在已经使用普通LayoutInfalter实例化的View上(xml必须是DataBinding风格的,普通LayoutInflater实例化布局时不会触发任何绑定机制,DataBindingUtil#bind才会发生绑定)
View view = LayoutInflater.from(context).inflate(R.layout.item_view,null,false);ItemViewBinding binding = DataBindingUtil.bind(view);复制代码

你在xml设置的变量他会在这个类中为你生成对应的GetterSetter.你可以调用它们给界面赋值,比如之前的我们定义的action.

//这里的代码是Java8的lambdamBinding.setAction(v->{    //TODO})复制代码

2.3.5 使用BR文件

它还会为你生成一个类似RBR文件,里面包含了你在DataBinding风格xml中定义的所有变量名的引用(由于使用的是APT生成,有时候需要Rebuild Project才能刷新),比如我们之前的action,它会为我们生成BR.action,我们可以这么使用它

mBinding.setVariable(BR.action,new View.OnClickListener(){    @Override    void onClick(View v){        //TODO    }})复制代码

2.3.6 传递复杂对象

在之前给xml中的变量中赋值时,我们用的都是一些类似String的简单对象,其实我们也可以定义一些复杂的对象,一次性传递到xml布局中

//javapublic class File{    public File(String name,                String size,                String path)                {                    this.name = name;                    this.size = size;                    this.path = path;                }    public final String name;    public final String size;    public final String path; }//xml            "file"            type="org.kexie.android.sample.bean.File"/>        "vertical"        android:layout_width="match_parent"        android:layout_height="match_parent"/>            "@{file.name}"                android:layout_width="match_parent"                android:layout_height="wrap_content"/>            "@{file.size}"                android:layout_width="match_parent"                android:layout_height="wrap_content"/>            "@{file.path}"                 android:layout_width="match_parent"                android:layout_height="wrap_content"/>    复制代码

个人认为绑定到xml中的数据最好是不可变的,所以上面的字段中我使用了final,但这不是必须的,根据你自己的需求来进行定制

2.3.7 绑定并非立即发生

这里有一点值得注意的是,你给ViewDataBinding的赋值并不是马上生效的,而是在当前方法执行完毕回到事件循环后,并保证在下一帧渲染之前得到执行,如果需要立即执行,请调用ViewDataBinding#executePendingBindings

2.3.8 使用android:id

如果你使用了android:id,那么这个View就也可以当成一个变量在下文的DataBinding表达式中使用,就像写Java.它还会帮你View绑定到ViewDataBinding中,你可以这么使用它们

    //xml    "@+id/my_text"        android:layout_width="match_parent"        android:layout_height="wrap_context"/>    "@+id/my_text2"        android:text="@{my_text.getText()}"        android:layout_width="match_parent"        android:layout_height="wrap_context"/>    //在java中my_text被去掉下划线,更符合java的命名习惯    mBinding.myText.setText("This is a new text");复制代码

用过ButterKnife的同学可能都知道,ButterKnife出过一次与gradle版本不兼容的事故,但是DataBinding是与gradle打包在一起发布的,一般不会出现这种问题,如果你不想用ButterKnife但有不想让DataBinding的风格的写法入侵你的xml太狠的话,只使用android:id将会是一个不错的选择.

2.4 正向绑定

某些第三方View是肯定没有适配DataBinding的,业界虽然一直说MVVM好,但现在MVP的开发方式毕竟还是主流,虽然这种情况我们可以用android:id,然后在Activity/Fragment中解决,但有时候我们想直接在xml中配置,以消除一些样板代码,这时候就需要自定义正向绑定.

2.4.1 自定义正向绑定适配器

我们可以使用@BindingAdapter自定义在xml中可使用的View属性,名字空间是不需要的,加了反而还会给你警告.

@Target(ElementType.METHOD)public @interface BindingAdapter {    /**     * 与此绑定适配器关联的属性。     */    String[] value();    /**     * 是否必须为每个属性分配绑定表达式,或者是否可以不分配某些属性。     * 如果为false,则当至少一个关联属性具有绑定表达式时,将调用BindingaAapter。     */    boolean requireAll() default true;}//@BindingAdapter需要一个静态方法,该方法的第一个参数是与该适配器兼容的View类型//从第二个参数开始,依次是你自定义的属性传进来的值.//使用requireAll来指定这些属性是全部需要,还是只要一个就可以//如果requireAll = false,触发适配器绑定时,没有被设置的属性将获得该类型的默认值//框架优先使用自定义的适配器处理绑定@BindingAdapter(value = {"load_async", "error_handler"},requireAll = true)public static void loadImage(ImageView view, String url, String error) {   Glide.with(view)        .load(url)        .error(Glide.with(view).load(error))        .into(view);}//在xml中使用它(下面那两个网址都不是实际存在的)"@{`http://android.kexie.org/image.png`}"    error_handler="@{`http://android.kexie.org/error.png`}"    android:layout_width="match_parent"    android:layout_height="match_parent"/>复制代码

2.4.2 第三方View适配

DataBinding风格的xml还能在一定程度上适配第三方View

    //如果你的自定义View中有这么一个Setter↓    public class RoundCornerImageView extends AppCompatImageView{        //......        public void setRadiusDp(float dp){            //TODO        }    }    //那么你可以在xml中使用radiusDp来使用它    "@{100}"        android:id="@+id/progress"        android:layout_width="match_parent"        android:layout_height="match_parent"        android:layout_gravity="center"        android:scaleType="centerCrop"        android:src="@drawable/progress"/>    //它会自己为你去找名称为setRadiusDp并且能接受100为参数的方法.复制代码

2.4.3 xml中的属性重定向

使用@BindingMethod来将xml属性重定向:

@Target(ElementType.ANNOTATION_TYPE)public @interface BindingMethod {    //需要重定向的View类型    Class type();    //需要重定向的属性名    String attribute();    //需要重定向到的方法名    String method();}//这是DataBinding源码中,DataBinding对于系统自带的TextView编写的适配器//这是androidx.databinding.adapters.TextViewBindingAdapter的源码@BindingMethods({        @BindingMethod(type = TextView.class, attribute = "android:autoLink", method = "setAutoLinkMask"),        @BindingMethod(type = TextView.class, attribute = "android:drawablePadding", method = "setCompoundDrawablePadding"),        @BindingMethod(type = TextView.class, attribute = "android:editorExtras", method = "setInputExtras"),        //......})public class TextViewBindingAdapter {    //......}//这样就可以建立起xml中属性与View中Setter的联系复制代码

2.4.4 添加转换层

使用@BindingConversion为添加转换层

@BindingConversionpublic static ColorDrawable toDrawable(int color) {     return new ColorDrawable(color); }//可以把color整形转换为android:src可接受的ColorDrawable类型//但是转换只适用于直接的赋值//如果你写了复杂的表达式,比如使用了?:这种三元运算符//那就照顾不到你了复制代码

2.5 反向绑定

有正向绑定就一定有反向绑定,正向绑定和反向绑定一起构成了双向绑定.

在我们之前编写的DataBinding表达式中,比如TextViewandroid:text之类的属性我们都是直接赋值一个String过去的,这就是正向绑定,我们给View的值能够直接反应到View上,而反向绑定就是View值的变化和也能反应给我们.

2.5.1 使用双向绑定

所有使用之前所有使用@{}包裹的都是正向绑定,而双向绑定是@={},并且只支持变量,字段,Setter(比如User#setName,就写@={user.name})的直接编写并且不支持复杂表达式

2.5.2 兼容LiveData与ObservableField

实际上,android:text不只能接受String,当使用双向绑定时,它也能接受MutableLiveDataObservableField作为赋值对象,这种赋值会将TextViewandroid:text的变化绑定到LiveData(实际上是MutableLiveData)或者是ObservableField上,以便我们在View的控制层(Activity/Fragment)更好地观察他们的变化.

当然除了ObservableFieldandroidx.databinding包下还有不装箱的ObservableInt,ObservableFloat等等.

但是为了支持LiveData我们必须开启第二版的DataBinding APT.

在你的gradle.properties添加

android.databinding.enableV2=true复制代码

现在我们可以通过LiveData(实际上是MutableLiveData)android:text的变化绑定到Activity/Fragment

//xml    "liveText"        type="MutableLiveData<String>">"@={text}"    android:layout_width="match_parent"    android:layout_height="wrap_context"/>//然后在Activity/Fragment中MutableLiveData liveText = new MutableLiveData();mBinding.setLiveText(liveText);liveText.observe(this,text->{   //TODO 观察View层变化 });复制代码

2.5.3 自定义反向绑定适配器

下面我们回到androidx.databinding.adapters.TextViewBindingAdapter的源码,继续对自定义反向绑定适配器进行分析.

    //我们可以看到源码中使用了@InverseBindingAdapter自定义了一个反向绑定器    //指定了其属性以及相关联的事件    @InverseBindingAdapter(attribute = "android:text", event = "android:textAttrChanged")    public static String getTextString(TextView view) {        return view.getText().toString();    }    //并为这个事件添加了一个可接受InverseBindingListener的属性    //为了说明方便,下面的代码已简化,源码并非如此,但主要逻辑相同    @BindingAdapter(value = {"android:textAttrChanged"})    public static void setTextWatcher(TextView view , InverseBindingListener textAttrChanged){        view.addTextChangedListener(new TextWatcher(){            //......             @Override            public void onTextChanged(CharSequence s, int start, int before, int count) {                textAttrChanged.onChange();                 }        });    }    //至此android:text的反向绑定完成    //当你使用@={}时实际上是用android:textAttrChanged属性向TextView设置了TextWatcher    //传入的InverseBindingListener是反向绑定监听器    //当调用InverseBindingListener的onChange时    //会调用@BindingAdapter所注解的方法将获得数据并写回到变量中.复制代码

2.6 配合DataBinding打造通用RecyclerView.Adapter

下面进行一个小小的实战吧,我们可以站在巨人的肩膀上造轮子.

    //导入万能适配器作为基类,可以大大丰富我们通用适配器的功能    implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.46'复制代码

由于基类很强大所以代码不多:

//X是泛型,可以是你在item中所使用的java beanpublic class GenericQuickAdapter        extends BaseQuickAdapter {    //BR中的变量名    protected final int mName;    //layoutResId是DataBinding风格的xml    public GenericQuickAdapter(int layoutResId, int name) {        super(layoutResId);        mName = name;        openLoadAnimation();    }    @Override    protected void convert(GenericViewHolder helper, X item) {        //触发DataBinding        helper.getBinding().setVariable(mName, item);    }    public static class GenericViewHolder extends BaseViewHolder {        private ViewDataBinding mBinding;        public GenericViewHolder(View view) {            super(view);            //绑定View获得ViewDataBinding            mBinding = DataBindingUtil.bind(view);        }        @SuppressWarnings("unchecked")        public  T getBinding() {            return (T) mBinding;        }    }}//实例化GenericQuickAdapter adapter = new GenericQuickAdapter<>(R.layout.item_file,BR.file);//在xml中使用起来就像这样            "file"            type="org.kexie.android.sample.bean.File"/>        "vertical"        android:layout_width="match_parent"        android:layout_height="match_parent"/>            "@{file.name}"                android:layout_width="match_parent"                android:layout_height="wrap_content"/>            "@{file.size}"                android:layout_width="match_parent"                android:layout_height="wrap_content"/>            "@{file.path}"                 android:layout_width="match_parent"                android:layout_height="wrap_content"/>    复制代码

3 Lifecycle

Android中,组件的管理组件的生命周期一直是一个比较麻烦的东西,而自Google推出Android Jetpack组件包以来,这个问题得到的比较妥善的解决,Lifecycle组件后来也成为Android Jetpack的核心。

3.1 导入

AndroidX为例,要使用Lifecycle组件,先在模块的build.gradle文件中添加依赖:

api 'androidx.lifecycle:lifecycle-extensions:2.1.0-alpha02'复制代码

由于Lifecycle组件由多个包构成,使用api导入时即可将其依赖的包全部导入该模块,包括commonlivedataprocessruntimeviewmodelservice等。

如果要使用Lifecycle中的注解,你还需要添加如下注解处理器,以便在编译时,完成对相应注解的处理。

annotationProcessor 'androidx.lifecycle:lifecycle-compiler:2.0.0'复制代码

对于一个App来说,使用Lifecycle组件是没有任何侵入性的,因为他已经天然的融合到Googleappcompat库中了,而如今无论是什么应用程序都几乎离不开appcompat,可以说集成Lifecycle只是启用了之前没用过的功能罢了。

3.2 LifecycleOwner

LifecycleOwnerLifecycle组件包中的一个接口,所有需要管理生命周期的类型都必须实现这个接口。

public interface LifecycleOwner{    /**    * Returns the Lifecycle of the provider.    *    * @return The lifecycle of the provider.    */    @NonNull    Lifecycle getLifecycle();}复制代码

但其实很多时候我们根本无需关心LifecycleOwner的存在。在Android中, FragmentActivityService都是具有生命周期的组件,但是Google已经让他们都实现了LifecycleOwner这个接口,分别是androdx.fragment.app.FragmentAppCompatActivityandroidx.lifecycle.LifecycleService.

在项目中,只要继承这些类型,可以轻松的通过LifecycleOwner#getLifecycle()获取到Lifecycle实例.这是一种解耦实现,LifecycleOwner不包含任何有关生命周期管理的逻辑,实际的逻辑都在Lifecycle实例中,我们可以通过传递Lifecycle实例而非LifecycleOwner来防止内存泄漏.

Lifecycle这个类的只有这三个方法:

@MainThreadpublic abstract void removeObserver(@NonNull LifecycleObserver observer);@MainThread@NonNullpublic abstract State getCurrentState();@MainThreadpublic abstract void addObserver(@NonNull LifecycleObserver observer); 复制代码

getCurrentState()可以返回当前该LifecycleOwner的生命周期状态,该状态与LifecycleOwner上的某些回调事件相关,只会出现以下几种状态,在Java中以一个枚举类抽象出来定义在Lifecycle类中。

public enum State{               DESTROYED,        INITIALIZED,        CREATED,        STARTED,        RESUMED;}复制代码
  • DESTROYED,在组件的onDestroy调用前,会变成该状态,变成此状态后将不会再出现任何状态改变,也不会发送任何生命周期事件

  • INITIALIZED,构造函数执行完成后但onCreate未执行时为此状态,是最开始时的状态

  • CREATED,在onCreate调用之后,以及onStop调用前会变成此状态

  • STARTED,在onStart调用之后,以及onPause调用前会变成此状态

  • RESUMED,再onResume调用之后会变成此状态

addObserver,此方法可以给LifecycleOwner添加一个观察者,来接收LifecycleOwner上的回调事件。回调事件也是一个枚举,定义在Lifecycle类中:

public enum Event{    /**    * Constant for onCreate event of the {@link LifecycleOwner}.    */    ON_CREATE,    /**    * Constant for onStart event of the {@link LifecycleOwner}.    */    ON_START,    /**    * Constant for onResume event of the {@link LifecycleOwner}.    */    ON_RESUME,    /**    * Constant for onPause event of the {@link LifecycleOwner}.    */    ON_PAUSE,    /**    * Constant for onStop event of the {@link LifecycleOwner}.    */    ON_STOP,    /**    * Constant for onDestroy event of the {@link LifecycleOwner}.    */    ON_DESTROY,    /**    * An {@link Event Event} constant that can be used to match all events.    */    ON_ANY }复制代码

每种事件都对应着Fragment/Activity中的事件。

3.3 LifecycleObserver

LifecycleObserver是生命周期的观察者,可能是这个包中我们最常用的接口了.

查看源码得知,他就是一个空接口,不包含任何实现,但是若我们想使用,还是得继承此接口。

public interface LifecycleObserver { }复制代码

继承LifecycleObserver后使用@OnLifecycleEvent注解(这时之前申明得注解处理器派上了用场),并设置需要监听的生命周期回调事件。

@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)public void test(){    ///TODO...}复制代码

然后在Activity/Fragment中:

getLifecycle().addObserver(yourLifecycleObserver);复制代码

即可在运行时收到相应的的回调事件,但是注意添加@OnLifecycleEvent注解的方法应该是包内访问权限或是public的,否则可能在编译时会报错,或者收不到回调。

若想在运行时移除LifecycleObserver,同样也还有Lifecycle#removeObserver方法。

4 LiveData

LiveData是对Android组件生命周期感知的粘性事件,也就是说,在LiveData持有数据时,你去订阅它就能收到他最后一次接收到的数据.在实战中,我们能用到的LiveData一般是它的两个子类MutableLiveDataMediatorLiveData.

4.1 LiveData基本使用

我们可以通过LiveData#observe来观察它所持有的值的变化,还可以通过LiveData#getValue来直接获取内部保存的值(非线程安全)

//LiveData 一般是用来给ViewModel保存数据的public class MyViewModel extends ViewModel{    private MutableLiveData mIsLoading = new MutableLiveData<>();    LiveData isLoading(){        return mIsLoading;    }}//Activity/Fragment观察ViewModelmViewModel.isLoading().observe(this, isLoading -> {    //TODO 发生在主线程,触发相关处理逻辑});//LiveData是依赖Lifecycle实现的//传入的this是LifecycleOwner//LiveData只会通知激活态的(STARTED和RESUMED)的LifecycleOwner//并且在Activity/Fragment被重建也能重新接收到LiveData保存的数据//在组件DESTROYED时,LiveData会把它移出观察者列表//当然你也可以不关联LifecycleOwner,让订阅一直保持.//需要这样时需要使用observeForevermViewModel.isLoading().observeForever(isLoading -> {    //TODO});//这个订阅永远不会被取消//除非你显示调用LiveData#removeObserver复制代码

4.2 MutableLiveData

顾名思义就是可变的LiveData,基类LiveData默认是不可变的,MutableLiveData开放了能够改变其内部所持有数据的接口.

public class MutableLiveData extends LiveData {    /**     * Creates a MutableLiveData initialized with the given {@code value}.     *     * @param value initial value     */    public MutableLiveData(T value) {        super(value);    }    /**     * Creates a MutableLiveData with no value assigned to it.     */    public MutableLiveData() {        super();    }    @Override    public void postValue(T value) {        super.postValue(value);    }    @Override    public void setValue(T value) {        super.setValue(value);    }}复制代码

分别是postValuesetValue,其中setValue内部检查线程是否为主线程,不允许在子线程中使用,用了就报错.postValue会将值通过主线程的Handler转发到主线程上.

LiveData可以有初始值,也可以没有,如果在没有初始值的情况下被订阅,则订阅者不会收到任何的值.

4.3 MediatorLiveData

MediatorLiveData继承自MutableLiveData,它主要用来实现多个LiveData数据源的合并.

public class MediatorLiveData extends MutableLiveData {    private SafeIterableMap, Source<?>> mSources = new SafeIterableMap<>();    @MainThread    public  void addSource(@NonNull LiveData source, @NonNull Observer<? super S> onChanged) {        Source e = new Source<>(source, onChanged);        Source<?> existing = mSources.putIfAbsent(source, e);        if (existing != null && existing.mObserver != onChanged) {            throw new IllegalArgumentException(                    "This source was already added with the different observer");        }        if (existing != null) {            return;        }        if (hasActiveObservers()) {            e.plug();        }    }    @MainThread    public  void removeSource(@NonNull LiveData toRemote) {        Source<?> source = mSources.remove(toRemote);        if (source != null) {            source.unplug();        }    }    @CallSuper    @Override    protected void onActive() {        for (Map.Entry, Source<?>> source : mSources) {            source.getValue().plug();        }    }    @CallSuper    @Override    protected void onInactive() {        for (Map.Entry, Source<?>> source : mSources) {            source.getValue().unplug();        }    }    private static class Source implements Observer {        final LiveData mLiveData;        final Observer<? super V> mObserver;        int mVersion = START_VERSION;        Source(LiveData liveData, final Observer<? super V> observer) {            mLiveData = liveData;            mObserver = observer;        }        void plug() {            mLiveData.observeForever(this);        }        void unplug() {            mLiveData.removeObserver(this);        }        @Override        public void onChanged(@Nullable V v) {            if (mVersion != mLiveData.getVersion()) {                mVersion = mLiveData.getVersion();                mObserver.onChanged(v);            }        }    }}复制代码

它比MutableLiveData多了两个方法addSourceremoveSource,通过这两个方法我们可以将其他LiveData合并到此LiveData上,当其他LiveData发生改变时,此LiveData就能收到通知.

@MainThreadpublic  void addSource(@NonNull LiveData source, @NonNull Observer<? super S> onChanged)@MainThreadpublic  void removeSource(@NonNull LiveData toRemote)复制代码

通过查看源码,我们可以知道在有观察者时LiveData#onActive会被回调,MediatorLiveData会在内部迭代,用observeForever订阅所有被合并进来的LiveData,这样就能接收所有LiveData的变化,在没有观察者时LiveData#onInactive会被回调,此时执行反操作removeObserver.

4.4 变换

使用androidx.lifecycle.Transformations这个工具类可以将持有一种类型的LiveData转换为另一种LiveData.他有类似于RxJava的使用方式.

LiveData boolLiveData = getBoolLiveData();LiveData stringLiveData = Transformations.map(boolLiveData,bool->Boolean.toString(bool));复制代码

上面只是一个演示,实际上可以执行更为复杂的逻辑,并且这种转换是惰性的,在没有激活态观察者时,这种转换不会发生.

5 ViewModel

5.1 自定义ViewModel

ViewModel其实没什么可说的,其源码主要的部分其实就只有这些

public abstract class ViewModel {    protected void onCleared() {    }}复制代码

简直一目了然,我们可以在ViewModel上使用LiveData作为字段保存数据,并编写业务逻辑(数据处理逻辑).就像这样

public class MyViewModel extends ViewModel{    public MutableLiveData username = new MutableLiveData<>();    public MutableLiveData password = new MutableLiveData<>();    public MutableLiveData text = new MutableLiveData<>();    public void action1(){        //TODO    }    public void initName(){        username.setValue("Luke Luo");    }    //......    @Override    protected void onCleared() {        //TODO 清理资源    }}复制代码

onCleared会在组件销毁的时候回调,我们可以重写这个方法在ViewModel销毁时添加一些自定义清理逻辑.

ViewModel还有一个子类AndroidViewModel也是一目了然,只是保存了Application实例而已.

public class AndroidViewModel extends ViewModel {    @SuppressLint("StaticFieldLeak")    private Application mApplication;    public AndroidViewModel(@NonNull Application application) {        mApplication = application;    }    /**     * Return the application.     */    @SuppressWarnings("TypeParameterUnusedInFormals")    @NonNull    public  T getApplication() {        //noinspection unchecked        return (T) mApplication;    }}复制代码

5.2 自定义ViewModel构造方式

我们可以通过ViewModelProviders来获取ViewModel,这样获取的ViewModel会绑定组件的生命周期(即在销毁时自动调用onCleared)

    mViewModel = ViewModelProviders.of(this).get(CustomViewModel.class);复制代码

AndroidLifecycle实现中框架向Activity中添加了一个继承了系统FragmentReportFragment来汇报组件的生命周期,如果你使用的是appcompatFragment,那么它对你就是不可见的,所以一定要避免使用系统的Fragment(在API28中已被标记为弃用).

ViewModel通过Lifecycle来管理自身释放,在组件的ON_DESTROY事件来到时,它的onCleared()也会被调用.

如果你想有自定义构造函数参数的ViewModel那你就得继承ViewModelProvider.AndroidViewModelFactory

//自定义构造函数的ViewModelpublic class NaviViewModel extends AndroidViewModel{    private AMapNavi mNavi;    public NaviViewModel(AMapNavi navi,Application application)    {        super(application);        mNavi = navi;    }    //......}//继承并重写createpublic final class NaviViewModelFactory        extends ViewModelProvider.AndroidViewModelFactory{    private final AMapNavi navi;    private final Application application;    public NaviViewModelFactory(@NonNull Context context, AMapNavi navi)    {        super((Application) context.getApplicationContext());        this.application = (Application) context.getApplicationContext();        this.navi = navi;    }    @NonNull    @Override    public  T create(@NonNull Class modelClass)    {        try        {            Constructor constructor = modelClass                    .getConstructor(Application.class, AMapNavi.class);            return constructor.newInstance(application, navi);        } catch (Exception e)        {            return super.create(modelClass);        }    }}//使用NaviViewModelFactory factory = new NaviViewModelFactory(context, navi);mViewModel = ViewModelProviders.of(this, factory).get(NaviViewModel.class);复制代码

说白了就是反射调用构造函数创建,也是一目了然.

6 RxJava

本篇文章只是针对响应式编程在MVVM体系下的应用,不对RxJava展开深度讨论,但是后面还会专门出一篇文章讨论RxJava的有关知识.

RxJavaMVVM中主要用于发布事件,下面是需要注意的一些点.

6.1 使用AutoDispose

RxJava是响应式编程这种思想在JVM这个平台上的实现,所以它一开始并没有为Android平台的特点而做出优化.

就像上面所介绍过的一样,Android的组件是有明确的生命周期的,如果在组件销毁后,RxJava仍有后台线程在运行且你的Observer引用了你的Activity,就会造成内存泄漏.

但其实RxJava是提供了释放机制的,那就是Disposeable,只不过这个实现这个机制的逻辑需要我们手动在Activity#onDestroy中进行硬编码,这会带来大量的样板代码.

为了解决这一局面,在Android Jetpack还没有诞生的时候,有大神开发了RxLifecycle,但是这个框架需要强制继承基类,对于一些现有项目的改造来说,其实是不太友好的,个人感觉并没有从根本上解决问题.

Android Jetpack诞生后AutoDispose给了我们另外一条出路.它使用RxJava2中的as运算符,将订阅者转换成能够自动释放订阅者对象.

在你的build.gradle中添加依赖:

implementation 'io.reactivex.rxjava2:rxjava:2.2.6'implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'implementation 'com.uber.autodispose:autodispose:1.1.0'implementation 'com.uber.autodispose:autodispose-android-archcomponents:1.1.0'复制代码

一个简单的示例:

Observable.just(new Object())            //使用AutoDispose#autoDisposable            //并使用AndroidLifecycleScopeProvider#form            //指定LifecycleOwner和需要在哪一个事件进行销毁            //关键↓是这行            .as(AutoDispose.autoDisposable(AndroidLifecycleScopeProvider.from(activity, Lifecycle.Event.ON_DESTROY)))            .subscribe();复制代码

上面代码的时间订阅将会在组件的Lifecycle.Event.ON_DESTROY事件来到时被释放,当然你也可以指定其他事件时释放.

6.2 防止多重点击

首先你可以使用JW大神的RxBinding来实现这一需求,但是今天我们不讨论RxBinding,因为网上的讨论RxBinding的文章已经太多了,随便抓一篇出来都已经非常优秀.

今天我们模仿RxBinding实现一个简单的,轻量化的,基于Java动态代理的,并且兼容所有第三方View所自定义Listener接口的防止多重点击机制.

二话不说先上代码:

import androidx.collection.ArrayMap;import androidx.lifecycle.Lifecycle;import androidx.lifecycle.LifecycleOwner;import com.uber.autodispose.AutoDispose;import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider;import io.reactivex.subjects.PublishSubject;import java.lang.reflect.Method;import java.lang.reflect.Proxy;import java.util.Map;import java.util.concurrent.TimeUnit;import static io.reactivex.android.schedulers.AndroidSchedulers.mainThread;public final class RxOnClick{    //默认最低的可取的时间    private static final int MINI_TIME = 200;    private final Class mInterface;    private X mInner;    private LifecycleOwner mOwner;    private int mTime;    private Lifecycle.Event mEvent;    private RxOnClick(Class type)    {        mInterface = type;    }    //从一个创建接口类型创建    public static  RxOnClick create(Class type)    {        return new RxOnClick<>(type);    }    //实际处理事件的Listener    public RxOnClick inner(X inner)    {        mInner = inner;        return this;    }    //依附于的组件也就是LifecycleOwner    public RxOnClick owner(LifecycleOwner owner)    {        mOwner = owner;        return this;    }    //只去time毫秒内的第一个结果作为有效结果    public RxOnClick throttleFirst(int time)    {        mTime = time;        return this;    }    //在哪一个事件进行释放    public RxOnClick releaseOn(Lifecycle.Event event)    {        mEvent = event;        return this;    }    //创建代理类实例    @SuppressWarnings("unchecked")    public X build()    {        //检查参数        if (mInterface == null || !mInterface.isInterface())        {            throw new IllegalArgumentException();        }        if (mTime < MINI_TIME)        {            mTime = MINI_TIME;        }        if (mEvent == null)        {            mEvent = Lifecycle.Event.ON_DESTROY;        }        if (mOwner == null || mInner == null)        {            throw new IllegalStateException();        }        //用反射遍历获取所有方法        Map> subjectMap = new ArrayMap<>();        for (Method method : mInterface.getDeclaredMethods())        {            PublishSubject subject = PublishSubject.create();            subject.throttleFirst(mTime, TimeUnit.MILLISECONDS)                    .observeOn(mainThread())                    .as(AutoDispose.autoDisposable(AndroidLifecycleScopeProvider.from(mOwner, mEvent)))                    .subscribe(args -> method.invoke(mInner, args));            subjectMap.put(method, subject);        }        //使用动态代理代理代理该接口并使用PublishSubject进行转发        return (X) Proxy.newProxyInstance(mInterface.getClassLoader(),                new Class[]{mInterface},                (proxy, method, args) -> {                    //Object类的方法直接调用                    if (Object.class.equals(method.getDeclaringClass()))                    {                        return method.invoke(proxy, args);                    }                    //否则转换为Rx事件流                    PublishSubject subject = subjectMap.get(method);                    if (subject != null)                    {                        subject.onNext(args);                    }                    return null;                });    }}复制代码

上面类在设计上采用了Builder模式,所以它实际是一个Builder.

其核心原理就是使用Java的动态代理机制创建Listener的代理类,代理类不处理事件,而是将事件通过PublishSubject(释放订阅后接收到的事件)转换为RxJava事件流推送到真正处理事件的Listener上.

这样我们就可以在这个事件流上对事件做手脚了,并且这样还能兼容RxBinding所不能兼容的第三方自定义View.

比如上面就加入了xxx毫秒内只取第一次点击和绑定组件的生命周期,用起来的时候就像是下面,依然非常简洁并且非常的有用:

        View.OnClickListener listener = RxOnClick                .create(View.OnClickListener.class)                .owner(this)                .inner(v -> {                    //TODO                })                .build();复制代码

7 使用MVVM改造Android现有体系

笔者就Android现有体系下的各种类库框架,通过自己实践的得出的经验将其进行如下归类,观点仅供参考,在实践中应该视项目特点进行适当进行改造.

7.1 View层

现有体系下的内容:

  • Activity/Fragment(布局生命周期与逻辑控制器)
  • android.view.View及其子类

设计原则:

  • View层不应该承担处理数据的责任,它应该只负责数据如何显示.
  • 它不应该直接持有Model层的任何引用,也不应该直接持有Model层的数据.
  • View层正常的行为应该是观察某个ViewModel,间接获取该ViewModelModel层中获取并处理过能在View层上直接显示的数据,数据由ViewModel保存,这样可以保证在Activity重建时页面上有关的数据不会丢失而且也不会造成View层与Model层的耦合.

7.2 DataBinding

现有体系下的内容:

  • Jetpack DataBinding 函数库
  • ViewAdapter
  • ......

设计原则:

  • 理想状态下,DataBindingView构建的关系应该是数据驱动的,即只要数据不改变View层实现的变更不会导致逻辑的重新编写(如把TextView改成EditText也不需要修改一行代码).
  • 虽然DataBinding函数库已经完成了大多数DataBinding应该做的事,但是不要为了数据驱动而排斥使用android:id来获取View并对View直接赋值,虽然这不够数据驱动,但是适当使用是可以的,毕竟AndroidView层目前还没有办法做到完全的数据驱动(主要是第三方库的兼容问题).
  • Adapter应该属于DataBinding的一种,与DataBinding函数库中生成的DataBinding相同,它也是使用数据来触发View层的改变.所以尽可能不要把它写到ViewModel中,但这不是必须的,做在对List操作要求比较高的情况下可以写到ViewModel中,但要保证一个原则——ViewModel应该只负责提供数据,而不应该知道这些数据要与何种View进行交互.

7.3 事件传递

现有体系下的内容:

  • EventBus事件总线
  • RxJava事件流

设计原则:

  • Jetpack中实现的LiveData能够很好的作为数据持有者,并且是生命周期感知的,但是有些时候我们需要向View层发送一些单次的数据,这时LiveData并不能够很好地工作.RxjavaEventBus是更好的选择.

7.4 ViewModel层

现有体系下的内容:

  • Jetpack ViewModel
  • Jetpack LiveData
  • 用于将Model数据转换成View能直接显示的数据的工具类
  • ......

设计原则:

  • ViewModel通常应该使用LiveData持有View层数据的实际控制权
  • ViewModel可以包含操作,但是ViewModel不应该直接或者间接地引用View,即使是方法中的参数也最好不要,因为ViewModel不应该知道自己到底是与哪一个View进行交互.
  • ViewModelModel的关系应该是——将Model层产生的数据翻译View层能够直接消化吸收的数据。
  • ViewModel可以向View层发送事件,然后View可以订阅这些事件以收到ViewModel层的通知.

7.5 Model层

现有体系下的内容:

  • 部分与Activity无关的系统服务
  • Room(SQLite数据库)
  • Retrofit(网络数据)
  • SharedPreferences
  • ......

设计原则:

  • 涉及Activity请一定不要包含进来,如WindowManager,它们属于View层.
  • Model层主要是原始数据的来源,由于存储格式/传输格式显示格式存在的巨大差异,View层往往并不能很好的直接消化这些数据,这时就需要一个中间人作为翻译,由此抽象出了ViewModel.

8 实战

我编写了一个简单的FTP客户端作为本次MVVM博文的演示Demo,该项目简单实践了QMUI+MVVM+DataBinding+RxJava+LiveData+Room的技术栈并由kotlinJava混编写成,支持断点续传,代码质量比较一般,有爱自取.

9 参考资料以及扩展阅读

  • DataBinding源码解析
  • Google官方黑科技 - DataBinding

10 结语

有些日子没更新文章了,最近发生了一些事让笔者彻底从无限的狂热中冷静下来,开始耐心做事.

本篇文章多达10000+字,感谢您在百忙之中抽空观看.所有内容均为个人学习总结与理解,仅供参考.

如果喜欢我的文章别忘了给我点个,拜托了这对我来说真的很重要.

转载于:https://juejin.im/post/5c973ac6f265da60f561199f

更多相关文章

  1. android dialog——自定义对话框之一
  2. 关于Android远程进程导致程序代码多次执行问题
  3. Android(安卓)SQlite数据库的使用(一)-一学就会android数据库
  4. Android学习——windows下搭建NDK_r9环境
  5. 读取Android(安卓)GPS NMEA数据
  6. Android系统数据传递机制
  7. Android(安卓)Activity横屏、竖屏、全屏
  8. 初学Android,Intent概要(十一)
  9. 使用Clojure构建原生Android应用

随机推荐

  1. 动态Android编程
  2. Android(安卓)对话框(Dialog)
  3. Android在任意位置由Notification跳向指
  4. 【Android(安卓)应用开发】 自定义组件
  5. android root实践
  6. Android之RecycleView实现指定范围的拖动
  7. 利用 Python 进行多 Sheet 表合并、多工
  8. Android知识点总结(十五) Android(安卓)MVP
  9. android BLE 4.0 setCharacteristicNotif
  10. Android系统横竖屏切换时候Activity的生