Android架构设计之MVC/MVP/MVVM浅析

目录

写在前面

一、案例演示

二、MVC模式

2.1、MVC简介

2.2、MVC模式的使用

2.3、MVC模式的缺点

三、MVP模式

3.1、MVP简介

3.2、MVP模式的作用

3.3、MVP模式的使用

3.4、MVP模式的缺点

四、MVVM模式

4.1、MVVM简介

4.2、Data Binding简介

4.3、Data Binding的常见使用

4.4、MVVM模式实现项目案例

4.5、MVVM模式的缺点


写在前面

3月底了,国内的疫情也快要过去了吧,恰逢此时,昨夜南京城下雪了,昨晚到家刚好是夜里12点整,站在阳台看着窗外,独自发出了一声感慨,只希望这场雪过后能迎来所有的美好!

回顾过去的一年,我发现自己都在忙着做业务,对自身的技能提升并没有多大的帮助,这让我有了深深的危机感,所以今年我首先是做了一个复盘,然后给自己定了一个目标,每周保证要写一篇文章,不管是对新知识的学习笔记还是对现有知识的复习总结,因为我本身是一个很笨的人,所以我只能用这种笨方法了,不积跬步无以至千里嘛!

说一下今天要写的东西吧,这一篇来总结一下安卓中常用的软件架构模式,这也是通往安卓中高级开发工程师必须要掌握的东西,当然也是面试中必考的一个知识点。今天先简单一点,先来说说最基础的三种,也是大家比较熟悉和容易掌握的——MVC/MVP/MVVM模式,后面我会继续分析组件化和插件化模式,大家敬请期待吧!

在写这篇文章之前,我使用较多的是MVC和MVP,但其实我更喜欢MVVM,因为它确实有很多优点,所以决定拿一个案例来总结一下各自的特点。我在网上搜了一下,看到很多文章都是写的登录案例,所以这个我就不写了,我写了一个列表请求加载展示数据的案例,这也是应用层最常见的业务场景了吧,项目中用了一些第三方的库,这里把地址贴上,方便大家查看对应的用法:

RxHttp——新一代网络请求神器:https://github.com/liujingxing/okhttp-RxHttp

RxLife——轻量级生命周期管理库:https://github.com/liujingxing/rxjava-RxLife

一、案例演示

案例已经上传到Github上面了,有需要的可以看看,欢迎star and fork,项目地址:https://github.com/JArchie/MVXProject

启动白屏问题我没有加,这个大家应该也都知道如何解决,这里主要是分析三种模式的写法,从上面这个效果图中我们可以看到,我分别使用了三种方式来实现这个列表的加载,效果是完全一样的,但是写法却不同,下面来详细分析。

二、MVC模式

2.1、MVC简介

MVC全称Model View Controller,也就是模型(model)-视图(view)-控制器(controller),M是指业务模型,V是指用户界面,C则是控制器。其中 View 层其实就是程序的 UI 界面,用于向用户展示数据以及接收用户的输入,而 Model 层就是 JavaBean 实体类,用于保存实例数据,Controller 控制器用于更新 UI 界面和数据实例。

2.2、MVC模式的使用

在安卓的MVC中,View层其实就是布局layout,我们是用XML来编写的,在这个案例中就是我们的列表RecyclerView了:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/mRecycler"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

可以看到xml的代码很简单,就一个recyclerview控件。

Model层上面也说了,在android中就是我们的数据实体,即JavaBean类,我们将拿到的接口返参定义到JavaBean中,这里偷了个懒,使用GsonFormat插件一键生成了,为了简便我把所有属性的setter和getter方法都给去除了,实际开发中还是要加上的,这里只是为了看的方便,不然代码很长:

package com.jarchie.mvc.model;

import java.util.List;

/**
 * 作者:created by Jarchie
 * 时间:2019-11-24 12:21:31
 * 邮箱:[email protected]
 * 说明:列表实体
 */
public class WxArticleBean {

    private DataBean data;
    private int errorCode;
    private String errorMsg;

    public static class DataBean {

        private int curPage;
        private int offset;
        private boolean over;
        private int pageCount;
        private int size;
        private int total;
        private List<DatasBean> datas;

        public static class DatasBean {

            private String apkLink;
            private int audit;
            private String author;
            private int chapterId;
            private String chapterName;
            private boolean collect;
            private int courseId;
            private String desc;
            private String envelopePic;
            private boolean fresh;
            private int id;
            private String link;
            private String niceDate;
            private String niceShareDate;
            private String origin;
            private String prefix;
            private String projectLink;
            private long publishTime;
            private int selfVisible;
            private long shareDate;
            private String shareUser;
            private int superChapterId;
            private String superChapterName;
            private String title;
            private int type;
            private int userId;
            private int visible;
            private int zan;
            private List<TagsBean> tags;

            public static class TagsBean {

                private String name;
                private String url;
            }
        }
    }
}

Controller层在android中实际上是Activity,我们来看一下具体的写法:

package com.jarchie.mvc.controller;

import android.annotation.SuppressLint;
import android.os.Bundle;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import com.jarchie.mvc.R;
import com.jarchie.mvc.adapter.WxArticleAdapter;
import com.jarchie.mvc.model.WxArticleBean;
import com.jarchie.mvc.constants.Constant;
import com.jarchie.mvc.view.LoadingView;
import com.rxjava.rxlife.RxLife;

import java.util.ArrayList;
import java.util.List;

import io.reactivex.android.schedulers.AndroidSchedulers;
import rxhttp.wrapper.param.RxHttp;

/**
 * 作者: 乔布奇
 * 日期: 2020-03-16 09:18
 * 邮箱: [email protected]
 * 描述: MVC模式搭建项目主页
 */
public class MainActivity extends AppCompatActivity {
    private LoadingView mLoadingView;
    private WxArticleAdapter mAdapter;
    private List<WxArticleBean.DataBean.DatasBean> mList = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
        initListener();
        initData();
    }

    //初始化数据
    @SuppressLint("NewApi")
    private void initData() {
        RxHttp.get(Constant.GET_ARTICAL_LIST)
                .asObject(WxArticleBean.class)
                .observeOn(AndroidSchedulers.mainThread())
                .doOnSubscribe(disposable -> mLoadingView.show())
                .doFinally(() -> mLoadingView.hide())
                .as(RxLife.as(this))
                .subscribe(wxArticleBean -> {
                    if (wxArticleBean.getData().getDatas().size() > 0) {
                        mList.addAll(wxArticleBean.getData().getDatas());
                        mAdapter.notifyDataSetChanged();
                    }
                }, throwable -> Toast.makeText(MainActivity.this, throwable.getMessage(), Toast.LENGTH_SHORT).show());
    }

    //初始化点击事件
    private void initListener() {
        mAdapter.setOnItemClickListener((bean, view, position) -> Toast.makeText(this, "当前点击的是第" + (position + 1) + "个条目", Toast.LENGTH_SHORT).show());
    }

    //初始化View
    private void initView() {
        RecyclerView mRecycler = findViewById(R.id.mRecycler);
        mLoadingView = new LoadingView(this);
        if (mAdapter == null) {
            mAdapter = new WxArticleAdapter(this, mList);
            mRecycler.setAdapter(mAdapter);
        } else {
            mAdapter.notifyDataSetChanged();
        }
        mRecycler.setLayoutManager(new LinearLayoutManager(this));
        mRecycler.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
    }
}

这里网络请求我使用的是RxHttp,底层也是OkHttp实现的,这个不用纠结,你用OkHttp,Retrofit这些都是一样的,实现形式每个人有每个人的方法,这里不需要过分关注,基本的业务逻辑在代码里也很清楚了,加载框加载--->发起请求网络接口--->请求成功或失败回调方法,加载框取消,成功展示数据并绑定适配,失败弹出失败提示。

2.3、MVC模式的缺点

传统的MVC模式Activity是充当Controller的角色,通过上面的代码我们不难发现一个问题,Activity里面既有跟View的交互,也有跟Model的交互,网络请求也掺杂在里面,由此可见业务耦合度较高,这还仅仅是加载一个简单的列表,实际业务场景绝不是这么简单的,所以就导致一个Activity里面充斥着大量的业务逻辑,Controller层的代码也会变的异常臃肿,毫不夸张的说有些哥们一个页面一千多行甚至几千行代码,所以安卓传统的MVC并不适用于日益发展的业务量,当然这个还是得分情况看,如果一个简单的小型项目使用这种模式也是OK的,中大型项目其实就很不推荐这种写法了,后期维护都很困难。所以,衍生了接下来要说的主角——MVP。

三、MVP模式

3.1、MVP简介

MVP模式是在MVC的基础上做了改进,最直接的思想是为了解耦。MVP是一种经典的模式,M代表Model,V代表View,P则是Presenter(Model和View之间的桥梁)。MVP模式的核心思想是把Activity中的UI逻辑抽象成View接口,把业务逻辑抽象成Presenter接口,Model类还是原来的Model类。

3.2、MVP模式的作用

1.分离视图逻辑和业务逻辑,降低耦合

2.Activity只处理生命周期的任务,代码简洁

3.视图逻辑和业务逻辑抽象到了View和Presenter中,提高阅读性

4.Presenter被抽象成接口,可以有多种具体的实现

5.业务逻辑在Presenter中,避免后台线程引用Activity导致内存泄漏

MVP模式是一种思想,一千个读者就有一千个哈姆雷特,所以实现形式每个人都是各有不同,大家在网上查找相关资料时,也不用过于纠结为什么每个人写的都不一样。

3.3、MVP模式的使用

先来说一下,MVP模式实现的过程中会有大量的接口,它的思想就是把UI逻辑和业务逻辑都抽象成接口,通过实现接口,在对应的方法或者数据结果回调给Activity。

Model层,Model主要是问服务端拿数据,本来是不想做这一层的,原本的JavaBean就属于Model层的范畴了,但是考虑到网络请求不止一个,实际项目中肯定有很多,所以这里定义一个Model类,将所有的网络请求都统一定义在这个类中,返回各个请求的Observable对象,方便快速查询和管理请求:

package com.jarchie.mvp.model;

import com.jarchie.mvp.bean.WxArticleBean;
import com.jarchie.mvp.constants.Constant;

import io.reactivex.Observable;
import rxhttp.wrapper.param.RxHttp;

/**
 * 作者: 乔布奇
 * 日期: 2020-03-17 10:46
 * 邮箱: [email protected]
 * 描述: 公共的Model类,返回各个请求的Obervable
 */
public class Model {

    //加载文章列表
    public static Observable<WxArticleBean> requestWxArticle(){
        return RxHttp.get(Constant.GET_ARTICAL_LIST)
                .asObject(WxArticleBean.class);
    }

}

View层,View层主要是将UI逻辑进行抽象,后面Activity实现该接口,然后在对应的方法中处理UI层的逻辑,我们这里封装一个BaseView,抽象出公共的View层:

package com.jarchie.mvp.base;

/**
 * 作者: 乔布奇
 * 日期: 2020-03-17 08:55
 * 邮箱: [email protected]
 * 描述: 抽象出公共View层
 */
public interface BaseView {

    //显示加载框
    void onShowLoading();

    //隐藏加载框
    void onDismissLoading();

    //加载失败回调
    void onLoadError(String errorMsg);

    //加载数据为空
    void onLoadEmpty();

}

我们再针对这个页面的具体业务逻辑定义一个接口,让这个接口继承我们的BaseView,先来分析一下View层需要处理什么业务,因为我们在Activity中需要拿到数据,将数据展示到页面上,所以这里定义一个方法,将网络请求到的数据回调给Activity页面,来看下具体的代码:

package com.jarchie.mvp.view;

import com.jarchie.mvp.base.BaseView;
import com.jarchie.mvp.bean.WxArticleBean;

import java.util.List;

/**
 * 作者: 乔布奇
 * 日期: 2020-03-17 09:48
 * 邮箱: [email protected]
 * 描述: 页面加载成功的回调接口
 */
public interface WxArticleView extends BaseView {
    //加载成功
    void onLoadSuccess(List<WxArticleBean.DataBean.DatasBean> mList);
}

Presenter层,P层主要是处理我们的业务逻辑,我们先抽象出一个公共的Presenter类,在这个类中,提供绑定View和解绑View的方法,有些朋友会注意到这个类还实现了BaseScope类,这个是RxLife中的一个类,RxLife是一款轻量级的RxJava生命周期管理库,配合RxHttp使用的,当Activity/Fragment销毁时,会自动关闭请求,可以跟随页面的生命周期走,防止出现内存泄漏的问题,来看下代码:

package com.jarchie.mvp.base;

import androidx.lifecycle.LifecycleOwner;

import com.rxjava.rxlife.BaseScope;

/**
 * 作者: 乔布奇
 * 日期: 2020-03-17 09:09
 * 邮箱: [email protected]
 * 描述: 抽象出公共的Presenter层
 */
public class BasePresenter<V extends BaseView> extends BaseScope {

    private V mView;

    public BasePresenter(LifecycleOwner owner) {
        super(owner);
    }

    //绑定View
    public void attachView(V view){
        this.mView = view;
    }

    //解绑View
    public void detachView(){
        this.mView = null;
    }

    //判断View是否绑定,在业务请求时调用该方法进行检查
    protected boolean isViewAttached(){
        return mView!=null;
    }

    //获取连接的View
    public V getView(){
        return mView;
    }

}

然后我们再针对具体业务来定义一个Presenter类,让它继承自BasePresenter,用来处理页面数据,并将数据通过之前View层中定义的接口回调给Activity,同时在请求开始、进行中、结束、失败等对应的状态下View层需要做的处理也都通过接口调用回调给Activity,具体来看代码:

package com.jarchie.mvp.activity;

import android.annotation.SuppressLint;
import android.os.Bundle;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import com.jarchie.mvp.R;
import com.jarchie.mvp.adapter.WxArticleAdapter;
import com.jarchie.mvp.bean.WxArticleBean;
import com.jarchie.mvp.customview.LoadingView;
import com.jarchie.mvp.presenter.WxArticlePresenter;
import com.jarchie.mvp.view.WxArticleView;

import java.util.List;
/**
 * 作者: 乔布奇
 * 日期: 2020-03-17 09:50
 * 邮箱: [email protected]
 * 描述: 主Activity页面
 */
@SuppressLint("NewApi")
public class MainActivity extends AppCompatActivity implements WxArticleView {
    private RecyclerView mRecycler;
    private LoadingView mLoadingView;
    private WxArticleAdapter mAdapter;
    private WxArticlePresenter mPresenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mPresenter = new WxArticlePresenter(this);
        mPresenter.attachView(this);
        initView();
        initData();
    }

    //初始化数据
    private void initData() {
        mPresenter.requestWxArticleData();
    }

    //初始化监听事件
    private void initListener() {
        mAdapter.setOnItemClickListener((bean, view, position) -> Toast.makeText(MainActivity.this, "当前点击的是第" + (position + 1) + "个条目", Toast.LENGTH_SHORT).show());
    }

    //初始化View
    private void initView() {
        mRecycler = findViewById(R.id.mRecycler);
        mLoadingView = new LoadingView(this);
    }

    @Override
    public void onLoadSuccess(List<WxArticleBean.DataBean.DatasBean> mList) {
        if (mAdapter == null) {
            mAdapter = new WxArticleAdapter(this, mList);
            mRecycler.setAdapter(mAdapter);
        } else {
            mAdapter.notifyDataSetChanged();
        }
        mRecycler.setLayoutManager(new LinearLayoutManager(this));
        mRecycler.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
        initListener();
    }

    @Override
    public void onShowLoading() {
        mLoadingView.show();
    }

    @Override
    public void onDismissLoading() {
        mLoadingView.hide();
    }

    @Override
    public void onLoadError(String errorMsg) {
        mLoadingView.hide();
        Toast.makeText(MainActivity.this, errorMsg, Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onLoadEmpty() {
        //可以显示个空布局,这里暂未处理
    }

    @Override
    protected void onDestroy() {
        mPresenter.detachView();
        super.onDestroy();
    }
}

3.4、MVP模式的缺点

这个其实了解MVP的人也都知道了,这种模式虽然是将各个层级的职责进行了划分,降低了代码的耦合度,但是从代码上可以看到,它大量的使用了接口,所以很直接的结果就是造成了接口类数量急剧上升,代码的复杂度和学习成本的提高。另一个角度,V层的渲染放在了P层,所以V层和P层的交互比较频繁,P层和V层之间的联系比较紧密,一旦V层修改则P层也需要修改。

四、MVVM模式

4.1、MVVM简介

MVVM模式包含三个部分,Model代表基本的业务逻辑,View显示内容,ViewModel将前面两者联系在一起。MVVM模式中,一个ViewModel和一个View匹配,它没有MVP中的IView接口,而是完全的和View绑定,所有View中的修改变化,都会自动更新到ViewModel中,同时ViewModel的任何变化也会自动同步到View上显示。

4.2、Data Binding简介

说到MVVM模式,那就不得不说一下DataBinding了。2015年I/O大会上谷歌介绍了一个非常牛B的工具,这个工具可以将View和一个对象的field绑定,当field更新的时候,framework将收到通知,然后View自动更新,这个工具就是DataBinding。Android Data Binding官方原生支持MVVM模型,可以让我们在不改变现有代码的框架下,非常容易的使用这些新特性。

DataBinding名为数据绑定,它的使命就是绑定数据。我们上面提到的MVC和MVP,其实或多或少的都存在着一些问题,究其原因实际上是Android本身的开发模式导致的,我们需要先监听数据的变化,然后将变化的数据同步更新到UI上,这样一次次的重复,MVC/MVP无论你怎么设计,其实本身并没有解决这个问题,而DataBinding的出现,恰恰解决了这个问题,你只需要将数据绑定到UI元素上,更新数据时UI会自动跟着改变,反之也是,你再也不用写那令人厌恶的findViewById()了,大大节省了Activity的代码量,数据可以单向或双向的绑定到layout文件中,有效的防止了内存泄漏的风险,还能自动进行空检测来避免空指针异常,下面就来说说DataBinding的常见使用。

4.3、Data Binding的常见使用

①、开启Data Binding

在Android中启用Data Binding的方法非常简单,你只需要在对应module的build.gradle文件中的android闭包下加入以下代码:

android {

    dataBinding{
        enabled = true
    }

}

②、告别findViewById()

布局文件的根布局也就是最外层需要使用<layout></layout>标签,然后在layout内部写上你需要的布局代码,在Activity中以前我们是需要使用setContentView来设置布局的加载,现在不用了,我们使用了Data Binding之后,rebuild一下项目,在工程中会自动给我们生成一个Binding类,比如我的布局文件名称是activity_main,那么生成的类就叫ActivityMainBinding类,然后我们通过:binding = DataBindingUtil.setContentView(this, R.layout.activity_main); 这行代码来加载布局,那这样就告别findViewById了,再也不用写一大堆恶心的代码了,直接通过binding.{viewId} 控件的id就可以找到控件了。

如果实在ListView或者RecyclerView适配器中使用数据绑定项,那么应该使用DataBindingUtil类的inflate()方法:    ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);

ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
举个例子:

<!--布局以layout作为根布局-->
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <!--我们需要展示的布局-->
    <LinearLayout 
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:id="@+id/mTest"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="告别findviewbyid" />
    </LinearLayout>

</layout>

③、绑定数据

要想绑定数据,首先我们需要在<layout></layout>标签里面写上<data></data>标签,在<data>标签内部是<variable/>标签,<variable/>标签内部指定name和typename是和java代码中的对象类似,名字可以自定义,type是和java代码中的类型类似,比如name="content",type="String",就表示可以使用name来绑定一个字符串,绑定时通过 @{数据类型的对象} 来控制对应的数据显示,实际项目中比如我们自定义了一个MyViewModel类,那么name可以随便写,比如就叫name="viewmodel",type需要指定我们的这个ViewModel类,比如type="com.jarchie.mvvm.MyViewModel",在具体数据绑定时,可以绑定MyViewModel中的对象的属性值,这里就先拿基本数据类型举个例子:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <variable
            name="content"
            type="String" />

        <variable
            name="enabled"
            type="boolean" />
    </data>

    <LinearLayout 
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:id="@+id/mTextView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:clickable="@{enabled}"
            android:text="@{content}" />
    </LinearLayout>
</layout>

④、绑定事件

绑定事件也很简单,比如我们要给一个TextView添加点击事件,我们在ViewModel类中写一个方法,比如:

//条目的点击事件
public void showToast() {
    Toast.makeText(mContext, "点击了", Toast.LENGTH_SHORT).show();
}

然后在布局文件里的这个控件的onClick属性中同样的使用@{}来指定这个点击事件,比如:

android:onClick="@{()->viewModel.showToast()}" ,这是lambda表达式的写法,OK,这样就完成了事件的绑定。

⑤、使用可观察数据对象

可观察性:

指一个对象将其数据变化通知给其他对象的能力。通过数据绑定库,您可以让对象、字段或集合变为可观察。任何 plain-old 对象都可用于数据绑定,但修改对象不会自动使界面更新。通过数据绑定,数据对象可在其数据发生更改时通知其他对象,即监听器。可观察类有三种不同类型:对象字段集合。当其中一个可观察数据对象绑定到界面并且该数据对象的属性发生更改时,界面会自动更新。

可观察字段:

在创建实现 Observable 接口的类时要完成一些操作,但如果您的类只有少数几个属性,则这样操作的意义不大。在这种情况下,您可以使用通用 Observable 类和以下特定于基元的类,将字段设为可观察字段:

ObservableBooleanObservableByteObservableCharObservableShortObservableIntObservableLong

ObservableFloatObservableDoubleObservableParcelable

举个栗子:

private static class User {
    public final ObservableField<String> firstName = new ObservableField<>();
    public final ObservableField<String> lastName = new ObservableField<>();
    public final ObservableInt age = new ObservableInt();
}

⑥、绑定适配器

先来看下什么是绑定适配器?这里我直接拿谷歌官方文档上的定义给大家看吧,省的我描述的不清楚:

最常用的是使用BindingAdapter提供自定义逻辑:

某些特性需要自定义绑定逻辑。例如,android:paddingLeft 特性没有关联的 setter,而是提供了 setPadding(left, top, right, bottom) 方法。使用 BindingAdapter 注释的静态绑定适配器方法支持自定义特性 setter 的调用方式。

举个常见的例子:通过子线程调用自定义加载程序来加载图片,如下代码所示:

<ImageView app:imageUrl="@{venue.imageUrl}" app:error="@{@drawable/venueError}" />
    

注意,@drawable/venueError 引用应用中的资源。使用 @{} 将资源括起来可使其成为有效的绑定表达式。

然后,我们需要再定义一个加载图片的方法,使用BindingAdapter来绑定数据:

@BindingAdapter({"imageUrl", "error"})
public static void loadImage(ImageView view, String url, Drawable error) {
    Picasso.get().load(url).error(error).into(view);
}

关于Data Binding的使用方式,我这里就先介绍这么多,具体更多更详细的内容请大家参考安卓官方开发文档中的介绍:

https://developer.android.google.cn/topic/libraries/data-binding

4.4、MVVM模式实现项目案例

接下来我们就来使用MVVM模式来实现上面用MVC/MVP做的那个小案例,代码其实也很简单,主要还是DataBinding的使用。

①、启用Data Binding

这一步在上面已经介绍过了,不多说:直接:dataBinding{ enabled = true }

②、为主页面布局创建ActivityMainModel类,在类中定义两个可观察字段,用来控制RecyclerView和空布局的显示与隐藏:

//空布局是否显示
public ObservableInt emptyVisibility = new ObservableInt(View.GONE);

//RecyclerView是否显示
public ObservableInt recyclerVisibility = new ObservableInt(View.GONE);

③、编写Activity页面布局

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <variable
            name="viewModel"
            type="com.jarchie.mvvm.viewmodel.MainViewModel" />
    </data>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:id="@+id/mEmpty"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:text="@string/empty_layout"
            android:gravity="center"
            android:textSize="18sp"
            android:visibility="@{viewModel.emptyVisibility}"/>

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/mRecycler"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:visibility="@{viewModel.recyclerVisibility}"/>
    </FrameLayout>

</layout>

④、加载主页面布局并绑定ViewModel

private ActivityMainBinding binding;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
    MainViewModel viewModel = new MainViewModel(this,this);
    binding.setViewModel(viewModel);
}

加载布局的代码其实上面介绍DB用法的时候都已经说过了,这里也不在多说了。

⑤、编写MainViewModel类,请求数据并回调给Activity页面展示:

package com.jarchie.mvvm.viewmodel;

import android.view.View;

import androidx.databinding.ObservableInt;
import androidx.lifecycle.LifecycleOwner;

import com.jarchie.mvvm.constants.Constant;
import com.jarchie.mvvm.model.WxArticleBean;
import com.rxjava.rxlife.BaseScope;
import com.rxjava.rxlife.RxLife;

import java.util.ArrayList;
import java.util.List;

import io.reactivex.android.schedulers.AndroidSchedulers;
import rxhttp.wrapper.param.RxHttp;

/**
 * 作者: 乔布奇
 * 日期: 2020-03-19 20:44
 * 邮箱: [email protected]
 * 描述: MainActivity的数据处理层
 */
public class MainViewModel extends BaseScope {

    //空布局是否显示
    public ObservableInt emptyVisibility = new ObservableInt(View.GONE);

    //RecyclerView是否显示
    public ObservableInt recyclerVisibility = new ObservableInt(View.GONE);

    //定义数据回调
    private DataListener mDataListener;
    private List<WxArticleBean.DataBean.DatasBean> mList = new ArrayList<>();

    public MainViewModel(LifecycleOwner owner, DataListener dataListener) {
        super(owner);
        this.mDataListener = dataListener;
        loadArticleData();
    }

    //加载数据
    private void loadArticleData() {
        RxHttp.get(Constant.GET_ARTICAL_LIST)
                .asObject(WxArticleBean.class)
                .observeOn(AndroidSchedulers.mainThread())
                .doOnSubscribe(disposable -> mDataListener.onShowLoading())
                .doFinally(() -> mDataListener.onDismissLoading())
                .as(RxLife.as(this))
                .subscribe(wxArticleBean -> {
                    if (wxArticleBean.getData() != null && wxArticleBean.getData().getDatas().size() > 0) {
                        emptyVisibility.set(View.GONE);
                        recyclerVisibility.set(View.VISIBLE);
                        mList.clear();
                        mList.addAll(wxArticleBean.getData().getDatas());
                        mDataListener.onDataChanged(mList);
                    } else {
                        emptyVisibility.set(View.VISIBLE);
                        recyclerVisibility.set(View.GONE);
                    }
                }, throwable -> mDataListener.onShowError(throwable.getMessage()));
    }

    //定义数据回调接口
    public interface DataListener {
        void onDataChanged(List<WxArticleBean.DataBean.DatasBean> list);

        void onShowLoading();

        void onDismissLoading();

        void onShowError(String msg);
    }

}

这里我们同样使用RxHttp请求服务端数据,将拿到的数据通过接口回调的形式给到Activity,这里同样继承BaseScope,使用到了RxLife绑定页面请求的生命周期,防止内存泄漏。

⑥、Activity页面加载展示数据

package com.jarchie.mvvm.view;

import android.annotation.SuppressLint;
import android.os.Bundle;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.DataBindingUtil;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;

import com.jarchie.mvvm.R;
import com.jarchie.mvvm.adapter.WxArticleAdapter;
import com.jarchie.mvvm.customview.LoadingView;
import com.jarchie.mvvm.databinding.ActivityMainBinding;
import com.jarchie.mvvm.model.WxArticleBean;
import com.jarchie.mvvm.viewmodel.MainViewModel;

import java.util.List;

/**
 * 作者: 乔布奇
 * 日期: 2020-03-17 09:35
 * 邮箱: [email protected]
 * 描述: 主Activity页面
 */
public class MainActivity extends AppCompatActivity implements MainViewModel.DataListener {
    private LoadingView mLoadingView;
    private ActivityMainBinding binding;
    private WxArticleAdapter mAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        mLoadingView = new LoadingView(this);
        MainViewModel viewModel = new MainViewModel(this,this);
        binding.setViewModel(viewModel);
    }

    @Override
    public void onDataChanged(List<WxArticleBean.DataBean.DatasBean> list) {
        if (mAdapter == null){
            mAdapter = new WxArticleAdapter(this,list);
            binding.mRecycler.setAdapter(mAdapter);
        }else {
            mAdapter.notifyDataSetChanged();
        }
        binding.mRecycler.setLayoutManager(new LinearLayoutManager(this));
        binding.mRecycler.addItemDecoration(new DividerItemDecoration(this,DividerItemDecoration.VERTICAL));
    }

    @Override
    public void onShowLoading() {
        mLoadingView.show();
    }

    @SuppressLint("NewApi")
    @Override
    public void onDismissLoading() {
        mLoadingView.hide();
    }

    @Override
    public void onShowError(String msg) {
        Toast.makeText(this,msg,Toast.LENGTH_SHORT).show();
    }

}

从上面的代码中可以看到,整个Activity页面的代码还是相当简洁的,那么写到这里大家是不是觉得该结束了呢?

当然没有,因为我们还没写完啊,Adapter同样需要使用ViewModel进行数据绑定的,所以接下来改造适配器。

⑦、编写Item的ViewModel类——ItemViewModel

package com.jarchie.mvvm.viewmodel;

import android.content.Context;
import android.text.TextUtils;
import android.widget.Toast;

import com.jarchie.mvvm.model.WxArticleBean;

/**
 * 作者: 乔布奇
 * 日期: 2020-03-19 22:01
 * 邮箱: [email protected]
 * 描述: 列表条目数据处理层
 */
public class ItemVIewModel {

    private Context mContext;
    private WxArticleBean.DataBean.DatasBean bean;

    public ItemVIewModel(Context context, WxArticleBean.DataBean.DatasBean bean) {
        this.mContext = context;
        this.bean = bean;
    }

    public void setDatasBean(WxArticleBean.DataBean.DatasBean bean) {
        this.bean = bean;
    }

    //获取标题
    public String getTitle() {
        return TextUtils.isEmpty(bean.getTitle()) ? "暂无" : bean.getTitle();
    }

    //获取来源
    public String getSuperChapterName() {
        return TextUtils.isEmpty(bean.getSuperChapterName()) ? "暂无" : bean.getSuperChapterName();
    }

    //获取推荐人
    public String getChapterName() {
        return TextUtils.isEmpty(bean.getChapterName()) ? "推荐人:暂无" : "推荐人:" + bean.getChapterName();
    }

    //获取链接地址
    public String getLink() {
        return TextUtils.isEmpty(bean.getLink()) ? "地址:暂无" : "地址:" + bean.getLink();
    }

    //条目的点击事件
    public void showToast() {
        Toast.makeText(mContext, "当前点击的是第" + bean.getPosition() + "个条目", Toast.LENGTH_SHORT).show();
    }

}

这里定义了几个getXXX()方法,用来设置数据到列表中的控件上,还定义了列表的点击事件。

⑧、来看下列表Item的布局文件,因为上面我们的方法中用的是getter方法,所以布局里面可以直接@{viewmode.field}设置:

<?xml version="1.0" encoding="utf-8" ?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <data>
        <variable
            name="viewModel"
            type="com.jarchie.mvvm.viewmodel.ItemVIewModel" />
    </data>

    <LinearLayout
        android:id="@+id/mAllLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:onClick="@{()->viewModel.showToast()}">

        <RelativeLayout
            android:id="@+id/mTopLayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <TextView
                android:id="@+id/mTitle"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="16dp"
                android:layout_marginTop="10dp"
                android:textColor="@color/colorPrimary"
                android:textSize="16sp"
                tools:text="标题"
                android:text="@{viewModel.title}"/>

            <TextView
                android:id="@+id/mSource"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_below="@+id/mTitle"
                android:layout_alignLeft="@+id/mTitle"
                android:layout_marginTop="10dp"
                android:layout_marginBottom="10dp"
                android:gravity="center"
                android:textColor="@color/colorPrimary"
                android:textSize="14sp"
                android:text="@{viewModel.superChapterName}"
                tools:text="来源" />
        </RelativeLayout>

        <LinearLayout
            android:id="@+id/mBottomLayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:layout_marginEnd="16dp"
            android:orientation="vertical">

            <View
                android:layout_width="match_parent"
                android:layout_height="1dp"
                android:background="@color/colorE9" />

            <TextView
                android:id="@+id/mReferee"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="10dp"
                android:layout_marginBottom="10dp"
                android:gravity="center_vertical"
                android:textColor="@color/colorAccent"
                android:textSize="14sp"
                android:text="@{viewModel.chapterName}"
                tools:text="推荐人" />

            <TextView
                android:id="@+id/mLink"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginBottom="10dp"
                android:gravity="center_vertical"
                android:textColor="@color/colorAccent"
                android:textSize="14sp"
                android:text="@{viewModel.link}"
                tools:text="www.baidu.com" />
        </LinearLayout>
    </LinearLayout>
</layout>

⑨、改造Adapter

package com.jarchie.mvvm.adapter;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.ViewGroup;

import androidx.annotation.NonNull;
import androidx.databinding.DataBindingUtil;
import androidx.recyclerview.widget.RecyclerView;

import com.jarchie.mvvm.R;
import com.jarchie.mvvm.databinding.ItemLayoutBinding;
import com.jarchie.mvvm.model.WxArticleBean;
import com.jarchie.mvvm.viewmodel.ItemVIewModel;

import java.util.List;

/**
 * 作者:created by Jarchie
 * 时间:2019-11-24 12:40:51
 * 邮箱:[email protected]
 * 说明:列表数据适配器
 */
public class WxArticleAdapter extends RecyclerView.Adapter<WxArticleAdapter.WxArticleHolder> {

    private Context mContext;
    private List<WxArticleBean.DataBean.DatasBean> mList;

    public WxArticleAdapter(Context context, List<WxArticleBean.DataBean.DatasBean> list) {
        this.mContext = context;
        this.mList = list;
    }

    @NonNull
    @Override
    public WxArticleHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        ItemLayoutBinding binding = DataBindingUtil.inflate(LayoutInflater.from(mContext), R.layout.item_layout, parent, false);
        return new WxArticleHolder(binding);
    }

    @Override
    public void onBindViewHolder(@NonNull final WxArticleHolder holder, final int position) {
        final WxArticleBean.DataBean.DatasBean bean = mList.get(position);
        bean.setPosition(position);
        holder.bindData(bean);
    }

    @Override
    public int getItemCount() {
        return mList == null ? 0 : mList.size();
    }

    class WxArticleHolder extends RecyclerView.ViewHolder {
        ItemLayoutBinding binding;

        WxArticleHolder(ItemLayoutBinding binding) {
            super(binding.mAllLayout);
            this.binding = binding;
        }

        //绑定数据
        void bindData(WxArticleBean.DataBean.DatasBean bean) {
            if (binding.getViewModel() == null) {
                binding.setViewModel(new ItemVIewModel(mContext,bean));
            } else {
                binding.getViewModel().setDatasBean(bean);
            }
        }
    }

}

在Adapter中加载布局这里我们使用的是DataBindUtil.inflate方法,上面也介绍过,在onBindViewHolder方法中,通过holder.bindData来绑定数据,接着往下跟进是在ViewHolder中定义这个bindData方法,传入的是数据实体,方法内部通过bing.setViewModel绑定ItemViewModel对象,ItemViewModel里面实现具体的数据绑定操作。

写到这里,我们的这个案例才算是写完了,整个流程都是通过Data Binding贯穿的,所以可见核心就是我们的Data Binding的使用了。

4.5、MVVM模式的缺点

MVVM模式虽然很好用,代码简介,使用起来优点很多,但是它不可能没有缺点啊,这里简单的说几条缺点吧:

①、调试BUG难度增加,数据绑定让一个BUG很快的被传递,可能是View层的问题,也可能是Model层的问题,所以变得不太好确定原始位置是在哪里。

②、较大模块中,Model也会很大,如果长期持有不释放,会造成内存的增加。

③、不利于代码重用,一个View绑定了一个Model,不同模块Model都是不同的,所以造成难以复用View。

时间也不早了,稍稍有点累了,今天的内容就写到这里吧。本篇是对MVC/MVP/MVVM这三种软件架构设计模式做了一个简单的分析和实战,实际项目肯定是比这复杂的多,重在方法和思想,虽然业务各有不同,实现起来其实都是一样的。

最后再来个传送门:https://github.com/JArchie/MVXProject

发布了48 篇原创文章 · 获赞 47 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/JArchie520/article/details/105161448