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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章