Android 開發(fā)中的MVC凌受,MVP和MVVM

原文出處:選擇恐懼癥的福音!教你認(rèn)清MVC思杯,MVP和MVVM/** **

相信大家對(duì)MVC胜蛉,MVP和MVVM都不陌生,作為三個(gè)最耳熟能詳?shù)腁ndroid框架色乾,它們的應(yīng)用可以是非常廣泛的誊册,但是對(duì)于一些新手來說,可能對(duì)于區(qū)分它們?nèi)齻€(gè)都有困難暖璧,更別說在實(shí)際的項(xiàng)目中應(yīng)用了案怯,有些時(shí)候想用MVP的,代碼寫著寫著就變成了MVC澎办,久而久之就對(duì)它們?nèi)齻€(gè)的選擇產(chǎn)生了恐懼感嘲碱,如果你也是這樣的人群,那么這篇文章可能會(huì)對(duì)你有很大的幫助局蚀,希望大家看完都會(huì)有收獲吧麦锯!

文章重點(diǎn):

(1)了解并區(qū)分MVC,MVP琅绅,MVVM扶欣。

(2)知道這三種模式在Android中如何使用。

(3)走出data binding的誤區(qū)。

(4)了解MVP+data binding的開發(fā)模式料祠。

本篇文章的demo我將會(huì)上傳到我的github上骆捧。

本文已獨(dú)家授權(quán)微信公眾號(hào) AndroidDeveloper ,拒絕其他任何形式的轉(zhuǎn)載

本文已獨(dú)家授權(quán)微信公眾號(hào) AndroidDeveloper 髓绽,拒絕其他任何形式的轉(zhuǎn)載

本文已獨(dú)家授權(quán)微信公眾號(hào) AndroidDeveloper 凑懂,拒絕其他任何形式的轉(zhuǎn)載

重要的事情說三遍

水之積也不厚,則其負(fù)大舟也無力
正如莊子在逍遙游中說的,如果水不夠深梧宫,那就沒有能夠擔(dān)負(fù)大船的力量 接谨。所以在真正開始涉及具體的代碼之前,我們要先對(duì)MVC塘匣,MVP和MVVM做一個(gè)初步的了解脓豪。如果各位同學(xué)對(duì)此已經(jīng)有所了解了,可以選擇性跳過這一節(jié)忌卤。

MVC

MVC扫夜,Model View Controller,是軟件架構(gòu)中最常見的一種框架驰徊,簡(jiǎn)單來說就是通過controller的控制去操作model層的數(shù)據(jù)笤闯,并且返回給view層展示,具體見下圖

mvc

當(dāng)用戶出發(fā)事件的時(shí)候棍厂,view層會(huì)發(fā)送指令到controller層颗味,接著controller去通知model層更新數(shù)據(jù),model層更新完數(shù)據(jù)以后直接顯示在view層上牺弹,這就是MVC的工作原理浦马。

那具體到Android上是怎么樣一個(gè)情況呢?

大家都知道一個(gè)Android工程有什么對(duì)吧张漂,有java的class文件晶默,有res文件夾,里面是各種資源航攒,還有類似manifest文件等等磺陡。對(duì)于原生的Android項(xiàng)目來說,layout.xml里面的xml文件就對(duì)應(yīng)于MVC的view層漠畜,里面都是一些view的布局代碼币他,而各種java bean,還有一些類似repository類就對(duì)應(yīng)于model層盆驹,至于controller層嘛圆丹,當(dāng)然就是各種activity咯滩愁。大家可以試著套用我上面說的MVC的工作原理是理解躯喇。比如你的界面有一個(gè)按鈕,按下這個(gè)按鈕去網(wǎng)絡(luò)上下載一個(gè)文件,這個(gè)按鈕是view層的廉丽,是使用xml來寫的倦微,而那些和網(wǎng)絡(luò)連接相關(guān)的代碼寫在其他類里,比如你可以寫一個(gè)專門的networkHelper類正压,這個(gè)就是model層欣福,那怎么連接這兩層呢?是通過button.setOnClickListener()這個(gè)函數(shù)焦履,這個(gè)函數(shù)就寫在了activity中拓劝,對(duì)應(yīng)于controller層。是不是很清晰嘉裤。

大家想過這樣會(huì)有什么問題嗎郑临?顯然是有的,不然為什么會(huì)有MVP和MVVM的誕生呢屑宠,是吧厢洞。問題就在于xml作為view層,控制能力實(shí)在太弱了典奉,你想去動(dòng)態(tài)的改變一個(gè)頁面的背景躺翻,或者動(dòng)態(tài)的隱藏/顯示一個(gè)按鈕,這些都沒辦法在xml中做卫玖,只能把代碼寫在activity中公你,造成了activity既是controller層,又是view層的這樣一個(gè)窘境假瞬。大家回想一下自己寫的代碼省店,如果是一個(gè)邏輯很復(fù)雜的頁面,activity或者fragment是不是動(dòng)輒上千行呢笨触?這樣不僅寫起來麻煩懦傍,維護(hù)起來更是噩夢(mèng)。(當(dāng)然看過Android源碼的同學(xué)其實(shí)會(huì)發(fā)現(xiàn)上千行的代碼不算啥芦劣,一個(gè)RecyclerView.class的代碼都快上萬行了呢粗俱。。)

MVC還有一個(gè)重要的缺陷虚吟,大家看上面那幅圖寸认,view層和model層是相互可知的,這意味著兩層之間存在耦合串慰,耦合對(duì)于一個(gè)大型程序來說是非常致命的偏塞,因?yàn)檫@表示開發(fā),測(cè)試邦鲫,維護(hù)都需要花大量的精力灸叼。

正因?yàn)镸VC有這樣那樣的缺點(diǎn)神汹,所以才演化出了MVP和MVVM這兩種框架。

MVP

MVP作為MVC的演化古今,解決了MVC不少的缺點(diǎn)屁魏,對(duì)于Android來說,MVP的model層相對(duì)于MVC是一樣的捉腥,而activity和fragment不再是controller層氓拼,而是純粹的view層,所有關(guān)于用戶事件的轉(zhuǎn)發(fā)全部交由presenter層處理抵碟。下面還是讓我們看圖

mvp

從圖中就可以看出桃漾,最明顯的差別就是view層和model層不再相互可知,完全的解耦拟逮,取而代之的presenter層充當(dāng)了橋梁的作用呈队,用于操作view層發(fā)出的事件傳遞到presenter層中,presenter層去操作model層唱歧,并且將數(shù)據(jù)返回給view層宪摧,整個(gè)過程中view層和model層完全沒有聯(lián)系÷溃看到這里大家可能會(huì)問几于,雖然view層和model層解耦了,但是view層和presenter層不是耦合在一起了嗎沿后?其實(shí)不是的沿彭,對(duì)于view層和presenter層的通信,我們是可以通過接口實(shí)現(xiàn)的尖滚,具體的意思就是說我們的activity喉刘,fragment可以去實(shí)現(xiàn)實(shí)現(xiàn)定義好的接口,而在對(duì)應(yīng)的presenter中通過接口調(diào)用方法漆弄。不僅如此睦裳,我們還可以編寫測(cè)試用的View,模擬用戶的各種操作撼唾,從而實(shí)現(xiàn)對(duì)Presenter的測(cè)試廉邑。這就解決了MVC模式中測(cè)試,維護(hù)難的問題倒谷。

當(dāng)然蛛蒙,其實(shí)最好的方式是使用fragment作為view層,而activity則是用于創(chuàng)建view層(fragment)和presenter層(presenter)的一個(gè)控制器渤愁。

MVVM

MVVM最早是由微軟提出的

mvvm

這里要感謝泡在網(wǎng)上的日子牵祟,因?yàn)榍懊婵吹降娜龔垐D我都是從它的博客中摘取的,如果有人知道不允許這樣做的話請(qǐng)告訴我抖格,我會(huì)從我的博客中刪除的诺苹,謝謝咕晋。

從圖中看出,它和MVP的區(qū)別貌似不大筝尾,只不過是presenter層換成了viewmodel層,還有一點(diǎn)就是view層和viewmodel層是相互綁定的關(guān)系办桨,這意味著當(dāng)你更新viewmodel層的數(shù)據(jù)的時(shí)候筹淫,view層會(huì)相應(yīng)的變動(dòng)ui。

我們很難去說MVP和MVVM這兩個(gè)MVC的變種孰優(yōu)孰劣呢撞,還是要具體情況具體分析损姜。

紙上得來終覺淺,絕知此事要躬行
對(duì)于程序員來說殊霞,空談是最沒效率的一種方式摧阅,相信大家看了我上面對(duì)于三種模式的分析,或多或少都會(huì)有點(diǎn)云里霧里绷蹲,下面讓我們結(jié)合代碼來看看棒卷。

讓我們?cè)囅胍幌孪旅孢@個(gè)情景,用戶點(diǎn)擊一個(gè)按鈕A祝钢,獲取github上對(duì)應(yīng)公司對(duì)應(yīng)倉庫中貢獻(xiàn)排行第一的任的名字比规,然后我們還會(huì)有一個(gè)按鈕B,用戶點(diǎn)擊按鈕B拦英,界面上排行第一的那個(gè)人的名字就會(huì)換成自己的蜒什。

MVC

MVC實(shí)現(xiàn)是最簡(jiǎn)單的。

首先看對(duì)應(yīng)view層的xml文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/container"
    android:orientation="vertical"
    tools:context=".ui.view.MainActivity"
    android:fitsSystemWindows="true">

    <Button
        android:text="get"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="get"/>

    <Button
        android:text="change"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="change"/>

    <TextView
        android:id="@+id/top_contributor"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:textSize="30sp"/>

</LinearLayout>

很簡(jiǎn)單疤估,兩個(gè)Button一個(gè)TextView

接著看對(duì)應(yīng)controller層的activity

public class MainActivity extends AppCompatActivity {

    private ProcessDialog dialog;
    private Contributor contributor = new Contributor();

    private TextView topContributor;

    private Subscriber<Contributor> contributorSub = new Subscriber<Contributor>() {

        @Override
        public void onStart() {
            showProgress();
        }

        @Override
        public void onCompleted() {

        }

        @Override
        public void onError(Throwable e) {

        }

        @Override
        public void onNext(Contributor contributor) {
            MainActivity.this.contributor = contributor;

            topContributor.setText(contributor.login);

            dismissProgress();
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        topContributor = (TextView)findViewById(R.id.top_contributor);
    }

    public void get(View view){
        getTopContributor("square", "retrofit");
    }

    public void change(View view){
        contributor.login = "zjutkz";

        topContributor.setText(contributor.login);
    }

    public void getTopContributor(String owner,String repo){
        GitHubApi.getContributors(owner, repo)
                .take(1)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeOn(Schedulers.newThread())
                .map(new Func1<List<Contributor>, Contributor>() {

                    @Override
                    public Contributor call(List<Contributor> contributors) {
                        return contributors.get(0);
                    }
                })
                .subscribe(contributorSub);
    }

    public void showProgress(){
        if(dialog == null){
            dialog = new ProcessDialog(this);
        }

        dialog.showMessage("正在加載...");
    }

    public void dismissProgress(){
        if(dialog == null){
            dialog = new ProcessDialog(this);
        }

        dialog.dismiss();
    }
}

我們看一下get()方法中調(diào)用的getTopContributor方法

public void getTopContributor(String owner,String repo){
    GitHubApi.getContributors(owner, repo)
            .take(1)
            .observeOn(AndroidSchedulers.mainThread())
            .subscribeOn(Schedulers.newThread())
            .map(new Func1<List<Contributor>, Contributor>() {

                @Override
                public Contributor call(List<Contributor> contributors) {
                    return contributors.get(0);
                }
            })
            .subscribe(contributorSub);
}

熟悉rxjava和retrofit的同學(xué)應(yīng)該都明白這是啥意思灾常,如果對(duì)這兩個(gè)開源庫不熟悉也沒事,可以參考給 Android 開發(fā)者的 RxJava 詳解和用 Retrofit 2 簡(jiǎn)化 HTTP 請(qǐng)求這兩篇文章铃拇。

對(duì)于這里大家只要知道這段代碼的意思就是去獲取github上owner公司中的repo倉庫里貢獻(xiàn)排名第一的那個(gè)人钞瀑。貢獻(xiàn)者是通過Contributor這個(gè)java bean存儲(chǔ)的。

public class Contributor {

    public String login;
    public int contributions;

    @Override
    public String toString() {
        return login + ", " + contributions;
    }
}

很簡(jiǎn)單慷荔,login表示貢獻(xiàn)者的名字仔戈,contributor表示貢獻(xiàn)的次數(shù)。

然后通過rxjava的subscriber中的onNext()函數(shù)得到這個(gè)數(shù)據(jù)拧廊。

private Subscriber<Contributor> contributorSub = new Subscriber<Contributor>() {

    @Override
    public void onStart() {
        showProgress();
    }

    @Override
    public void onCompleted() {

    }

    @Override
    public void onError(Throwable e) {

    }

    @Override
    public void onNext(Contributor contributor) {
        MainActivity.this.contributor = contributor;

        topContributor.setText(contributor.login);

        dismissProgress();
    }
};

至于另外那個(gè)change按鈕的工作大家應(yīng)該都看得懂监徘,這里不重復(fù)了。

好了吧碾,我們來回顧一遍整個(gè)流程凰盔。

首先在xml中寫好布局代碼。

其次倦春,activity作為一個(gè)controller户敬,里面的邏輯是監(jiān)聽用戶點(diǎn)擊按鈕并作出相應(yīng)的操作落剪。比如針對(duì)get按鈕,做的工作就是調(diào)用GithubApi的方法去獲取數(shù)據(jù)尿庐。

GithubApi忠怖,Contributor等類則表示MVC中的model層,里面是數(shù)據(jù)和一些具體的邏輯操作抄瑟。

說完了流程再來看看問題凡泣,還記得我們前面說的嗎,MVC在Android上的應(yīng)用皮假,一個(gè)具體的問題就是activity的責(zé)任過重鞋拟,既是controller又是view。這里是怎么體現(xiàn)的呢惹资?看了代碼大家發(fā)現(xiàn)其中有一個(gè)progressDialog贺纲,在加載數(shù)據(jù)的時(shí)候顯示,加載完了以后取消褪测,邏輯其實(shí)是view層的邏輯猴誊,但是這個(gè)我們沒辦法寫到xml里面啊,包括TextView.setTextView()侮措,這個(gè)也一樣稠肘。我們只能把這些邏輯寫到activity中,這就造成了activity的臃腫萝毛,這個(gè)例子可能還好项阴,如果是一個(gè)復(fù)雜的頁面呢?大家自己想象一下笆包。

MVP

通過具體的代碼大家知道了MVC在Android上是如何工作的环揽,也知道了它的缺點(diǎn),那MVP是如何修正的呢庵佣?

這里先向大家推薦github上的一個(gè)第三方庫歉胶,通過這個(gè)庫大家可以很輕松的實(shí)現(xiàn)MVP。好了巴粪,還是看代碼吧通今。

首先還是xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/container"
    android:orientation="vertical"
    tools:context=".ui.view.MainActivity"
    android:fitsSystemWindows="true">

    <Button
        android:text="get"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="get"/>

    <Button
        android:text="change"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="change"/>

    <TextView
        android:id="@+id/top_contributor"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:textSize="30sp"/>

</LinearLayout>

這個(gè)和MVC是一樣的,畢竟界面的形式是一樣的嘛肛根。

接下去辫塌,我們看一個(gè)接口。

public interface ContributorView extends MvpView {

    void onLoadContributorStart();

    void onLoadContributorComplete(Contributor topContributor);

    void onChangeContributorName(String name);
}

這個(gè)接口起什么作用呢派哲?還記得我之前說的嗎臼氨?MVP模式中,view層和presenter層靠的就是接口進(jìn)行連接芭届,而具體的就是上面的這個(gè)了储矩,里面定義的三個(gè)方法感耙,第一個(gè)是開始獲取數(shù)據(jù),第二個(gè)是獲取數(shù)據(jù)成功持隧,第三個(gè)是改名即硼。我們的view層(activity)只要實(shí)現(xiàn)這個(gè)接口就可以了。

下面看activity的代碼

public class MainActivity extends MvpActivity<ContributorView,ContributorPresenter> implements ContributorView {

    private ProcessDialog dialog;

    private TextView topContributor;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        topContributor = (TextView)findViewById(R.id.top_contributor);
    }

    @NonNull
    @Override
    public ContributorPresenter createPresenter() {
        return new ContributorPresenter();
    }

    public void get(View view){
        getPresenter().get("square", "retrofit");
    }

    public void change(View view){
        getPresenter().change();
    }

    @Override
    public void onLoadContributorStart() {
        showProgress();
    }

    @Override
    public void onLoadContributorComplete(Contributor contributor) {

        topContributor.setText(contributor.toString());

        dismissProgress();
    }

    @Override
    public void onChangeContributorName(String name) {
        topContributor.setText(name);
    }

    public void showProgress(){
        if(dialog == null){
            dialog = new ProcessDialog(this);
        }

        dialog.showMessage("正在加載...");
    }

    public void dismissProgress(){
        if(dialog == null){
            dialog = new ProcessDialog(this);
        }

        dialog.dismiss();
    }
}

它繼承自MvpActivity屡拨,實(shí)現(xiàn)了剛才的ContributorView接口只酥。繼承的那個(gè)MvpActivity大家這里不用太關(guān)心主要是做了一些初始化和生命周期的封裝。我們只要關(guān)心這個(gè)activity作為view層洁仗,到底是怎么工作的层皱。

public void get(View view){
    getPresenter().get("square", "retrofit");
}

public void change(View view){
    getPresenter().change();
}

get()和change()這兩個(gè)方法是我們點(diǎn)擊按鈕以后執(zhí)行的性锭,可以看到赠潦,里面完完全全沒有任何和model層邏輯相關(guān)的東西,只是簡(jiǎn)單的委托給了presenter草冈,那我們?cè)倏纯磒resenter層做了什么

public class ContributorPresenter extends MvpBasePresenter<ContributorView> {

    private Subscriber<Contributor> contributorSub = new Subscriber<Contributor>() {

        @Override
        public void onStart() {
            ContributorView view = getView();
            if(view != null){
                view.onLoadContributorStart();
            }
        }

        @Override
        public void onCompleted() {

        }

        @Override
        public void onError(Throwable e) {

        }

        @Override
        public void onNext(Contributor topContributor) {
            ContributorView view = getView();
            if(view != null){
                view.onLoadContributorComplete(topContributor);
            }
        }
    };

    public void get(String owner,String repo){
        GitHubApi.getContributors(owner, repo)
                .take(1)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeOn(Schedulers.newThread())
                .map(new Func1<List<Contributor>, Contributor>() {

                    @Override
                    public Contributor call(List<Contributor> contributors) {
                        return contributors.get(0);
                    }
                })
                .subscribe(contributorSub);
    }

    public void change(){
        ContributorView view = getView();
        if(view != null){
            view.onChangeContributorName("zjutkz");
        }
    }
}

其實(shí)就是把剛才MVC中activity的那部分和model層相關(guān)的邏輯抽取了出來她奥,并且在相應(yīng)的時(shí)機(jī)調(diào)用ContributorView接口對(duì)應(yīng)的方法,而我們的activity是實(shí)現(xiàn)了這個(gè)接口的怎棱,自然會(huì)走到對(duì)應(yīng)的方法中哩俭。

好了,我們來捋一捋拳恋。

首先凡资,和MVC最大的不同,MVP把a(bǔ)ctivity作為了view層谬运,通過代碼也可以看到隙赁,整個(gè)activity沒有任何和model層相關(guān)的邏輯代碼,取而代之的是把代碼放到了presenter層中梆暖,presenter獲取了model層的數(shù)據(jù)之后伞访,通過接口的形式將view層需要的數(shù)據(jù)返回給它就OK了。

這樣的好處是什么呢轰驳?首先厚掷,activity的代碼邏輯減少了,其次级解,view層和model層完全解耦冒黑,具體來說,如果你需要測(cè)試一個(gè)http請(qǐng)求是否順利勤哗,你不需要寫一個(gè)activity薛闪,只需要寫一個(gè)java類,實(shí)現(xiàn)對(duì)應(yīng)的接口俺陋,presenter獲取了數(shù)據(jù)自然會(huì)調(diào)用相應(yīng)的方法豁延,相應(yīng)的昙篙,你也可以自己在presenter中mock數(shù)據(jù),分發(fā)給view層诱咏,用來測(cè)試布局是否正確苔可。

MVVM

首先在看這段內(nèi)容之前,你需要保證你對(duì)data binding框架有基礎(chǔ)的了解袋狞。不了解的同學(xué)可以去看下這篇文章焚辅。在接下去讓我們開始探索MVVM,MVVM最近在Android上可謂十分之火苟鸯,最主要的原因就是谷歌推出了data binding這個(gè)框架同蜻,可以輕松的實(shí)現(xiàn)MVVM。但是早处,我在網(wǎng)上查閱關(guān)于Android的data binding資料的時(shí)候湾蔓,發(fā)現(xiàn)國內(nèi)有很多人都誤解了,首先砌梆,我們從一篇錯(cuò)誤的文章開始默责。當(dāng)然我在這里引用這篇文章也是對(duì)事不對(duì)人,如果對(duì)文章的作者產(chǎn)生了不好的影響我這里說一聲抱歉咸包。

上面那篇文章是一個(gè)關(guān)于data binding的使用桃序,看起來很美好,但是烂瘫,其中有一個(gè)錯(cuò)誤可以說是非常媒熊,非常,非常嚴(yán)重的坟比。

wrong_viewmodel

它竟然說data binding的viewmodel層是binding類芦鳍,其實(shí)不止是這篇文章,其他有一些開發(fā)者寫的關(guān)于data binding的文章里都犯了一樣的錯(cuò)誤温算。大家如果也有這樣的概念怜校,請(qǐng)務(wù)必糾正過來!注竿!

說完了錯(cuò)誤的概念茄茁,那data binding中真正的viewmodel是什么呢?我們還是以之前MVC巩割,MVP的那個(gè)例子做引導(dǎo)裙顽。

首先是view層,這沒啥好說的宣谈,和MVP一樣愈犹,只不過多了數(shù)據(jù)綁定。view層就是xml和activity。

<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable name="contributor" type="zjutkz.com.mvvm.viewmodel.Contributor"/>
    </data>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/container"
        android:orientation="vertical"
        android:fitsSystemWindows="true">

        <Button
            android:id="@+id/get"
            android:text="get"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="get"/>

        <Button
            android:id="@+id/change"
            android:text="change"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="change"/>

        <TextView
            android:id="@+id/top_contributor"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center"
            android:textSize="30sp"
            android:text="@{contributor.login}"/>
    </LinearLayout>

</layout>
public class MainActivity extends AppCompatActivity {

    private Subscriber<Contributor> contributorSub = new Subscriber<Contributor>() {

        @Override
        public void onStart() {
            showProgress();
        }

        @Override
        public void onCompleted() {

        }

        @Override
        public void onError(Throwable e) {

        }

        @Override
        public void onNext(Contributor contributor) {
            binding.setContributor(contributor);

            dismissProgress();
        }
    };

    private ProcessDialog dialog;

    private MvvmActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = DataBindingUtil.setContentView(this, R.layout.mvvm_activity_main);
    }

    public void get(View view){
        getContributors("square", "retrofit");
    }

    public void change(View view){
        if(binding.getContributor() != null){
            binding.getContributor().setLogin("zjutkz");
        }
    }

    public void showProgress(){
        if(dialog == null){
            dialog = new ProcessDialog(this);
        }

        dialog.showMessage("正在加載...");
    }

    public void dismissProgress(){
        if(dialog == null){
            dialog = new ProcessDialog(this);
        }

        dialog.dismiss();
    }

    public void getContributors(String owner,String repo){
        GitHubApi.getContributors(owner, repo)
                .take(1)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeOn(Schedulers.newThread())
                .map(new Func1<List<Contributor>, Contributor>() {

                    @Override
                    public Contributor call(List<Contributor> contributors) {
                        return contributors.get(0);
                    }
                })
                .subscribe(contributorSub);
    }
}

如果你對(duì)data binding框架是有了解的漩怎,上面的代碼你能輕松的看懂勋颖。

那model層又是什么呢?當(dāng)然就是那些和數(shù)據(jù)相關(guān)的類勋锤,GithubApi等等饭玲。

重點(diǎn)來了,viewmodel層呢叁执?好吧茄厘,viewmodel層就是是Contributor類!大家不要驚訝谈宛,我慢慢的來說次哈。

public class Contributor extends BaseObservable{

    private String login;
    private int contributions;

    @Bindable
    public String getLogin(){
        return login;
    }

    @Bindable
    public int getContributions(){
        return contributions;
    }

    public void setLogin(String login){
        this.login = login;
        notifyPropertyChanged(BR.login);
    }

    public void setContributions(int contributions){
        this.contributions = contributions;
        notifyPropertyChanged(BR.contributions);
    }

    @Override
    public String toString() {
        return login + ", " + contributions;
    }
}

我們可以看到,Contributor和MVP相比吆录,繼承自了BaseObservable窑滞,有基礎(chǔ)的同學(xué)都知道這是為了當(dāng)Contributor內(nèi)部的variable改變的時(shí)候ui可以同步的作出響應(yīng)。

我為什么說Contributor是一個(gè)viewmodel呢径筏。大家還記得viewmodel的概念嗎葛假?view和viewmodel相互綁定在一起障陶,viewmodel的改變會(huì)同步到view層滋恬,從而view層作出響應(yīng)。這不就是Contributor和xml中那些組件元素的關(guān)系嗎抱究?所以恢氯,大家不要被binding類迷惑了,data binding框架中的viewmodel是自己定義的那些看似是model類的東西鼓寺!比如這里的Contributor勋拟!

話說到這里,那binding類又是什么呢妈候?其實(shí)具體對(duì)應(yīng)到之前MVVM的那張圖就很好理解了敢靡,我們想一下,binding類的工作是什么苦银?

binding = DataBindingUtil.setContentView(this, R.layout.mvvm_activity_main);

binding.setContributor(contributor);

首先啸胧,binding要通過DataBindingUtil.setContentView()方法將xml,也就是view層設(shè)定幔虏。

接著纺念,通過setXXX()方法將viewmodel層注入進(jìn)去。

由于這兩個(gè)工作想括,view層(xml的各個(gè)組件)和viewmodel層(contributor)綁定在了一起陷谱。

好了,大家知道了嗎瑟蜈,binding類烟逊,其實(shí)就是上圖中view和viewmodel中間的那根線霸堋!宪躯!

真理在荒謬被證實(shí)以前图毕,都只是暗室里的裝飾
前面討論了MVC,MVP和MVVM具體的實(shí)現(xiàn)方案眷唉,大家肯定都了解了它們?nèi)叩年P(guān)系和使用方式予颤。但是,這里我想說冬阳,不要把一個(gè)框架看作萬能的蛤虐,其實(shí)MVP和MVVM都是有自己的缺陷的!下面我一一來說肝陪。

MVP

MVP的問題在于驳庭,由于我們使用了接口的方式去連接view層和presenter層,這樣就導(dǎo)致了一個(gè)問題氯窍,如果你有一個(gè)邏輯很復(fù)雜的頁面饲常,你的接口會(huì)有很多,十幾二十個(gè)都不足為奇狼讨。想象一個(gè)app中有很多個(gè)這樣復(fù)雜的頁面贝淤,維護(hù)接口的成本就會(huì)非常的大。

這個(gè)問題的解決方案就是你得根據(jù)自己的業(yè)務(wù)邏輯去斟酌著寫接口政供。你可以定義一些基類接口播聪,把一些公共的邏輯,比如網(wǎng)絡(luò)請(qǐng)求成功失敗布隔,toast等等放在里面离陶,之后你再定義新的接口的時(shí)候可以繼承自那些基類,這樣會(huì)好不少衅檀。

MVVM

MVVM的問題呢招刨,其實(shí)和MVC有一點(diǎn)像。data binding框架解決了數(shù)據(jù)綁定的問題哀军,但是view層還是會(huì)過重沉眶,大家可以看我上面那個(gè)MVVM模式下的activity

public class MainActivity extends AppCompatActivity {

    private Subscriber<Contributor> contributorSub = new Subscriber<Contributor>() {

        @Override
        public void onStart() {
            showProgress();
        }

        @Override
        public void onCompleted() {

        }

        @Override
        public void onError(Throwable e) {

        }

        @Override
        public void onNext(Contributor contributor) {
            binding.setContributor(contributor);

            dismissProgress();
        }
    };

    private ProcessDialog dialog;

    private MvvmActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = DataBindingUtil.setContentView(this, R.layout.mvvm_activity_main);
    }

    public void get(View view){
        getContributors("square", "retrofit");
    }

    public void change(View view){
        if(binding.getContributor() != null){
            binding.getContributor().setLogin("zjutkz");
        }
    }

    public void showProgress(){
        if(dialog == null){
            dialog = new ProcessDialog(this);
        }

        dialog.showMessage("正在加載...");
    }

    public void dismissProgress(){
        if(dialog == null){
            dialog = new ProcessDialog(this);
        }

        dialog.dismiss();
    }

    public void getContributors(String owner,String repo){
        GitHubApi.getContributors(owner, repo)
                .take(1)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeOn(Schedulers.newThread())
                .map(new Func1<List<Contributor>, Contributor>() {

                    @Override
                    public Contributor call(List<Contributor> contributors) {
                        return contributors.get(0);
                    }
                })
                .subscribe(contributorSub);
    }
}

大家有沒有發(fā)現(xiàn),activity在MVVM中應(yīng)該是view層的排苍,但是里面卻和MVC一樣寫了對(duì)model的處理沦寂。有人會(huì)說你可以把對(duì)model的處理放到viewmodel層中,這樣不是更符合MVVM的設(shè)計(jì)理念嗎淘衙?這樣確實(shí)可以传藏,但是progressDialog的show和dismiss呢?你怎么在viewmodel層中控制?這是view層的東西啊毯侦,而且在xml中也沒有哭靖,我相信會(huì)有解決的方案,但是我們有沒有一種更加便捷的方式呢侈离?

路漫漫其修遠(yuǎn)兮试幽,吾將上下而求索
其實(shí),真正的最佳實(shí)踐都是人想出來的卦碾,我們?yōu)楹尾唤Y(jié)合一下MVP和MVVM的特點(diǎn)呢铺坞?其實(shí)谷歌已經(jīng)做了這樣的事,大家可以看下這個(gè)洲胖。沒錯(cuò)济榨,就是MVP+data binding,我們可以使用presenter去做和model層的通信绿映,并且使用data binding去輕松的bind data擒滑。還是讓我們看代碼吧。

首先還是view層叉弦。

<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable name="contributor" type="zjutkz.com.mvpdatabinding.viewmodel.Contributor"/>
    </data>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/container"
        android:orientation="vertical"
        android:fitsSystemWindows="true">

        <Button
            android:id="@+id/get"
            android:text="get"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="get"/>

        <Button
            android:id="@+id/change"
            android:text="change"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="change"/>

        <TextView
            android:id="@+id/top_contributor"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center"
            android:textSize="30sp"
            android:text="@{contributor.login}"/>
    </LinearLayout>

</layout>
public class MainActivity extends MvpActivity<ContributorView,ContributorPresenter> implements ContributorView {

    private ProcessDialog dialog;

    private ActivityMainBinding binding;

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

    @NonNull
    @Override
    public ContributorPresenter createPresenter() {
        return new ContributorPresenter();
    }

    public void get(View view){
        getPresenter().get("square", "retrofit");
    }

    public void change(View view){
        if(binding.getContributor() != null){
            binding.getContributor().setLogin("zjutkz");
        }
    }

    @Override
    public void onLoadContributorStart() {
        showProgress();
    }

    @Override
    public void onLoadContributorComplete(Contributor contributor) {
        binding.setContributor(contributor);
        dismissProgress();
    }

    public void showProgress(){
        if(dialog == null){
            dialog = new ProcessDialog(this);
        }

        dialog.showMessage("正在加載...");
    }

    public void dismissProgress(){
        if(dialog == null){
            dialog = new ProcessDialog(this);
        }

        dialog.dismiss();
    }
}

然后是presenter層

public class ContributorPresenter extends MvpBasePresenter<ContributorView> {

    private Subscriber<Contributor> contributorSub = new Subscriber<Contributor>() {

        @Override
        public void onStart() {
            ContributorView view = getView();
            if(view != null){
                view.onLoadContributorStart();
            }
        }

        @Override
        public void onCompleted() {

        }

        @Override
        public void onError(Throwable e) {

        }

        @Override
        public void onNext(Contributor topContributor) {
            ContributorView view = getView();
            if(view != null){
                view.onLoadContributorComplete(topContributor);
            }
        }
    };

    public void get(String owner,String repo){
        GitHubApi.getContributors(owner, repo)
                .take(1)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeOn(Schedulers.newThread())
                .map(new Func1<List<Contributor>, Contributor>() {

                    @Override
                    public Contributor call(List<Contributor> contributors) {
                        return contributors.get(0);
                    }
                })
                .subscribe(contributorSub);
    }
}

model層就是GithubApi等等丐一。

我們使用了data binding框架去節(jié)省了類似findViewById和數(shù)據(jù)綁定的時(shí)間,又使用了presenter去將業(yè)務(wù)邏輯和view層分離淹冰。

當(dāng)然這也不是固定的库车,你大可以在viewmodel中實(shí)現(xiàn)相應(yīng)的接口,presenter層的數(shù)據(jù)直接發(fā)送到viewmodel中榄棵,在viewmodel里更新凝颇,因?yàn)関iew和viewmodel是綁定的潘拱,這樣view也會(huì)相應(yīng)的作出反應(yīng)疹鳄。

說到這里,我還是想重復(fù)剛才的那句話芦岂,最佳實(shí)踐都是人想出來的瘪弓,用這些框架根本的原因也是為了盡量低的耦合性和盡量高的可復(fù)用性。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末禽最,一起剝皮案震驚了整個(gè)濱河市腺怯,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌川无,老刑警劉巖呛占,帶你破解...
    沈念sama閱讀 219,589評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異懦趋,居然都是意外死亡晾虑,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,615評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來帜篇,“玉大人糙捺,你說我怎么就攤上這事◇舷叮” “怎么了洪灯?”我有些...
    開封第一講書人閱讀 165,933評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)竟痰。 經(jīng)常有香客問我签钩,道長(zhǎng),這世上最難降的妖魔是什么坏快? 我笑而不...
    開封第一講書人閱讀 58,976評(píng)論 1 295
  • 正文 為了忘掉前任边臼,我火速辦了婚禮,結(jié)果婚禮上假消,老公的妹妹穿的比我還像新娘柠并。我一直安慰自己,他們只是感情好富拗,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,999評(píng)論 6 393
  • 文/花漫 我一把揭開白布臼予。 她就那樣靜靜地躺著,像睡著了一般啃沪。 火紅的嫁衣襯著肌膚如雪粘拾。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,775評(píng)論 1 307
  • 那天创千,我揣著相機(jī)與錄音缰雇,去河邊找鬼。 笑死追驴,一個(gè)胖子當(dāng)著我的面吹牛械哟,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播殿雪,決...
    沈念sama閱讀 40,474評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼暇咆,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼熏瞄!你這毒婦竟也來了橱脸?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,359評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤恐疲,失蹤者是張志新(化名)和其女友劉穎亏镰,沒想到半個(gè)月后扯旷,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,854評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡索抓,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,007評(píng)論 3 338
  • 正文 我和宋清朗相戀三年钧忽,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了某抓。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,146評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡惰瓜,死狀恐怖否副,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情崎坊,我是刑警寧澤备禀,帶...
    沈念sama閱讀 35,826評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站奈揍,受9級(jí)特大地震影響曲尸,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜男翰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,484評(píng)論 3 331
  • 文/蒙蒙 一另患、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蛾绎,春花似錦昆箕、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,029評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至顽爹,卻和暖如春纤泵,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背镜粤。 一陣腳步聲響...
    開封第一講書人閱讀 33,153評(píng)論 1 272
  • 我被黑心中介騙來泰國打工捏题, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人肉渴。 一個(gè)月前我還...
    沈念sama閱讀 48,420評(píng)論 3 373
  • 正文 我出身青樓公荧,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親黄虱。 傳聞我的和親對(duì)象是個(gè)殘疾皇子稚矿,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,107評(píng)論 2 356

推薦閱讀更多精彩內(nèi)容