Android MVP 十分鐘入門皇帮!

前言##

在日常開發(fā)APP 的過程中卷哩,隨著業(yè)務(wù)的擴展,規(guī)模的變化属拾。我們的代碼規(guī)模也會逐漸變得龐大将谊,每一個類里的代碼也會逐漸增多冷溶。尤其是Activity和Fragment ,由于Context 的存在瓢娜,基本上所有對視圖的操作我們只能在Activity和Fragment中完成挂洛;即便是對某些邏輯進行封裝,Activity和Fragment 依舊會顯得過于臃腫眠砾。因此虏劲,我們需要換一種思路去寫代碼,這個時候MVP模式就應(yīng)用而生了褒颈!那么MVP 怎么用呢柒巫,下面就來說一說。

假設(shè)你現(xiàn)在如要實現(xiàn)下圖中的功能:

這個需求很簡單谷丸,就是點擊按鈕堡掏,下載一張圖片,顯示下載進度刨疼;下載完成后泉唁,在ImageView中顯示這張圖片。
下面我們就分別用傳統(tǒng)的方式(也就是所謂的MVC)和MVP 模式分別取實現(xiàn)這個功能揩慕。然后分析一下MVP 到底好在哪里亭畜。

MVC##

public class MVCActivity extends AppCompatActivity {

    private Context mContext;
    private ImageView mImageView;
    private MyHandler mMyHandler;
    private ProgressDialog progressDialog;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_mvc);
        mContext = this;
        init();
    }

    private void init() {
        //view init
        mImageView = (ImageView) findViewById(R.id.image);
        mMyHandler = new MyHandler();

        progressDialog = new ProgressDialog(mContext);
        progressDialog.setButton(DialogInterface.BUTTON_NEGATIVE, "Cancle", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                progressDialog.dismiss();
            }
        });
        progressDialog.setCanceledOnTouchOutside(false);
        progressDialog.setTitle("下載文件");
        progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);


        //click-event
        findViewById(R.id.downloadBtn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                progressDialog.show();
                HttpUtil.HttpGet(Constants.DOWNLOAD_URL, new DownloadCallback(mMyHandler));
            }
        });

        findViewById(R.id.downloadBtn1).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                progressDialog.show();
                HttpUtil.HttpGet(Constants.DOWNLOAD_ERROR_URL, new DownloadCallback(mMyHandler));
            }
        });

    }


    class MyHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case 300:
                    int percent = msg.arg1;
                    if (percent < 100) {
                        progressDialog.setProgress(percent);
                    } else {
                        progressDialog.dismiss();
                        Glide.with(mContext).load(Constants.LOCAL_FILE_PATH).into(mImageView);
                    }
                    break;
                case 404:
                    progressDialog.dismiss();
                    Toast.makeText(mContext, "Download fail !", Toast.LENGTH_SHORT).show();
                    break;
                default:
                    break;
            }
        }
    }
}

用mvc的方式,一個Activity就能搞定迎卤。代碼邏輯很簡單拴鸵,點擊按鈕后顯示之前初始化好ProgressDialog析既,然后開始下載任務(wù)(這里HttpUtil 內(nèi)部簡單封裝了OKHttp 的異步GET請求正驻,實現(xiàn)下載文件保存到本地的功能,實現(xiàn)細節(jié)在此不做深入探討陨闹,有興趣的同學(xué)可以查看源碼)樟凄,然后將請求結(jié)果通過Handler返回聘芜,在handleMessage中根據(jù)返回數(shù)據(jù)的信息做出不同的UI 處理;下載成功時在ImageView中顯示圖片缝龄,下載失敗時Toast提示汰现。

可以發(fā)現(xiàn),在這種情況之前二拐,Activity的任務(wù)十分繁重服鹅,既要負責下載任務(wù)的具體實施,還要根據(jù)下載進行再次的邏輯判斷百新,才能去更新UI企软。這里只是一個簡單的任務(wù),你可能覺得無所謂饭望,但是實際開發(fā)中仗哨,一個Activity中有許多的交互事件形庭,這個時候Activity的代碼就顯得特別的龐大;一旦需求變更或出現(xiàn)bug厌漂,那簡直就是噩夢一場萨醒。

因此,我們希望Activity可以變成下面這樣

  • 他負責發(fā)起處理和用戶交互的內(nèi)容苇倡,但又不負責具體的實現(xiàn)富纸;
  • 需要顯示什么,不顯示什么旨椒,什么東西顯示多少晓褪,有個東西可以直接告訴他,
  • Activity不再做復(fù)雜的邏輯處理综慎;

具體到上面的demo里就是涣仿,Activity負責發(fā)起下載任務(wù),但是不負責具體實現(xiàn)示惊;什么時候顯示ProgressDialog好港,顯示多少?什么時候提示錯誤信息米罚,這一切都希望有個東西能直接告訴Activity钧汹,而不再是在Activity里再做判斷。怎樣才能做到呢阔拳?那就得靠MVP 了崭孤。

MVP

MVP 模式所做的事情很簡單类嗤,就是將業(yè)務(wù)邏輯和視圖邏輯抽象到接口中糊肠。

怎么理解呢,我們就根據(jù)此次要實現(xiàn)的下載功能遗锣,用代碼說話货裹。

定義Model,View精偿,Presenter 接口###

Model Interface####

Model 接口定義所有需要實現(xiàn)的業(yè)務(wù)邏輯弧圆,在我們的下載任務(wù)中,業(yè)務(wù)邏輯只有一個笔咽,就是下載搔预;因此Model 接口可以這么定義 :

public interface IDownloadModel {
    /**
     * 下載操作
     * @param url
     */
    void download(String url);
}

View Interface####

View 接口定義所有需要實現(xiàn)的視圖邏輯,在我們的下載任務(wù)中叶组,視圖邏輯包括

  • 顯示ProgressDialog拯田;
  • 顯示Dialog具體進度;
  • 顯示具體的View(設(shè)置圖片)甩十;
  • 顯示錯誤信息(Toast提示)

因此View接口可以這么定義:

public interface IDownloadView {
    /**
     * 顯示進度條
     * @param show
     */
    void showProgressBar(boolean show);

    /**
     * 設(shè)置進度條進度
     * @param progress
     */
    void setProcessProgress(int progress);

    /**
     * 根據(jù)數(shù)據(jù)設(shè)置view
     * @param result
     */
    void setView(String result);

    /**
     * 設(shè)置請求失敗時的view
     */
    void showFailToast();
}

Presenter Interface####

Presenter 接口作為連接Model和View的中間橋梁船庇,需要將二者連接起來吭产,因此他需要完成以下工作:

  • 執(zhí)行下載任務(wù)
  • 下載成功返回下載結(jié)果
  • 下載過程返回下載進度
  • 下載失敗回調(diào)

因此,Presenter 就可以這么定義:

public interface IDowndownPresenter {
    /**
     * 下載
     * @param url
     */
    void download(String url);

    /**
     * 下載成功
     * @param result
     */
    void downloadSuccess(String result);

    /**
     * 當前下載進度
     * @param progress
     */
    void downloadProgress(int progress);

    /**
     * 下載失敗
     */
    void downloadFail();
}

接口Model鸭轮,View臣淤,Presenter 具體實現(xiàn)###

上面實現(xiàn)了,各個接口的定義窃爷,下面來看看他們具體的實現(xiàn):

Model 具體實現(xiàn)####

public class DownloadModel implements IDownloadModel {
    private IDowndownPresenter mIDowndownPresenter;
    private MyHandler mMyHandler = new MyHandler();

    public DownloadModel(IDowndownPresenter IDowndownPresenter) {
        mIDowndownPresenter = IDowndownPresenter;
    }

    @Override
    public void download(String url) {
        HttpUtil.HttpGet(url, new DownloadCallback(mMyHandler));
    }

    class MyHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case 300:
                    int percent = msg.arg1;
                    if (percent < 100) {
                        mIDowndownPresenter.downloadProgress(percent);
                    } else {
                        mIDowndownPresenter.downloadSuccess(Constants.LOCAL_FILE_PATH);
                    }
                    break;
                case 404:
                    mIDowndownPresenter.downloadFail();
                    break;
                default:
                    break;
            }
        }
    }
}

在MVP模式中邑蒋,Model的工作就是完成具體的業(yè)務(wù)操作,網(wǎng)絡(luò)請求按厘,持久化數(shù)據(jù)增刪改查等任務(wù)寺董。同時Model中又不會包含任何View。
這里Model的具體實現(xiàn)很簡單刻剥,將Http任務(wù)的結(jié)果返回到Handler當中遮咖,而在Handler中的實現(xiàn)又是由Presenter完成。那么Presenter接口又是怎樣實現(xiàn)的呢造虏?趕緊來看看

Presenter 具體實現(xiàn)####

public class DownloadPresenter implements IDowndownPresenter {
    private IDownloadView mIDownloadView;
    private IDownloadModel mIDownloadModel;


    public DownloadPresenter(IDownloadView IDownloadView) {
        mIDownloadView = IDownloadView;
        mIDownloadModel = new DownloadModel(this);
    }

    @Override
    public void download(String url) {
        mIDownloadView.showProgressBar(true);
        mIDownloadModel.download(url);
    }

    @Override
    public void downloadSuccess(String result) {
        mIDownloadView.showProgressBar(false);
        mIDownloadView.setView(result);
    }

    @Override
    public void downloadProgress(int progress) {
        mIDownloadView.setProcessProgress(progress);
    }

    @Override
    public void downloadFail() {
        mIDownloadView.showProgressBar(false);
        mIDownloadView.showFailToast();
    }
}

可以看到御吞,我們在DownloadPresenter的構(gòu)造方法中,同時實例化了Model和View漓藕,這樣Presenter中就同時包含了兩者陶珠;這樣;在Presenter具體實現(xiàn)中享钞,業(yè)務(wù)相關(guān)的操作由Model去完成(例如download)揍诽,視圖相關(guān)的操作由View去完成(如setView等)。Presenter 作為橋梁的作用就這樣體現(xiàn)出來了栗竖,巧妙的將View和Model的具體實現(xiàn)連接了起來暑脆。

View具體實現(xiàn)####

最后再看一下View接口的具體實現(xiàn),也就是Activity的實現(xiàn):

public class MVPActivity extends AppCompatActivity implements IDownloadView {
    private Context mContext;
    private ImageView mImageView;
    private ProgressDialog progressDialog;

    private DownloadPresenter mDownloadPresenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mContext = this;
        setContentView(R.layout.activity_mvp);
        init();
    }

    private void init() {
        mDownloadPresenter = new DownloadPresenter(this);
        //view init
        mImageView = (ImageView) findViewById(R.id.image);
        findViewById(R.id.downloadBtn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mDownloadPresenter.download(Constants.DOWNLOAD_URL);
            }
        });

        findViewById(R.id.downloadBtn1).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mDownloadPresenter.download(Constants.DOWNLOAD_ERROR_URL);
            }
        });

        progressDialog = new ProgressDialog(mContext);
        progressDialog.setButton(DialogInterface.BUTTON_NEGATIVE, "Cancle", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                progressDialog.dismiss();
            }
        });
        progressDialog.setCanceledOnTouchOutside(false);
        progressDialog.setTitle("下載文件");
        progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);

    }

    @Override
    public void showProgressBar(boolean show) {
        if (show) {
            progressDialog.show();
        } else {
            progressDialog.dismiss();
        }
    }

    @Override
    public void setProcessProgress(int progress) {
        progressDialog.setProgress(progress);
    }

    @Override
    public void setView(String result) {
        Glide.with(mContext).load(result).into(mImageView);
    }

    @Override
    public void showFailToast() {
        Toast.makeText(mContext, "Download fail !", Toast.LENGTH_SHORT).show();
    }
}

在點下按鈕執(zhí)行開始下載任務(wù)的時候狐肢,View(Activity)中沒有具體的實現(xiàn)添吗,只是調(diào)用了Presenter中的download方法,而Presenter中的download又會去調(diào)用Model的download方法份名,Model又會在根據(jù)具體邏輯(在這里就是Http請求)的狀態(tài)去調(diào)用Presenter中的方法碟联,例如我們在handleMessage方法中,調(diào)用mIDowndownPresenter.downloadProgress(percent)時僵腺,就會去調(diào)用Presenter的具體實現(xiàn)

    @Override
    public void downloadProgress(int progress) {
        mIDownloadView.setProcessProgress(progress);
    }

而他的內(nèi)部實現(xiàn)又是操作具體的View鲤孵,也就是我們在Activity中初始化Presenter中傳遞的this,也就是當前Activity(View)辰如,這樣最終回到了Activity中的


    @Override
    public void setProcessProgress(int progress) {
        progressDialog.setProgress(progress);
    }

我們?yōu)閜rogressDialog 設(shè)置進度普监。

至此,我們就通過MVP 的模式實現(xiàn)了我們之前所設(shè)想的Activity

  • Button的click方法負責發(fā)起下載任務(wù),但又不負責具體實現(xiàn)鹰椒,而是由Presenter轉(zhuǎn)接給Model去實現(xiàn)
  • Activity 什么時候顯示ProgressDialog锡移,什么時候顯示Toast直接由Presenter告訴他,他只做一個View想做的事情
  • Activity里沒有任何邏輯處理漆际,所有的邏輯判斷都在Model中完成了淆珊。

這就是MVP !<榛恪施符!

MVC VS MVP##

通過上面的兩種實現(xiàn)方案,相信每個人都已經(jīng)理解了MVC和MVP的區(qū)別擂找;下面就其各自的優(yōu)缺點再做一下總結(jié)戳吝;當然,這里的優(yōu)缺點只是相對而言贯涎。

優(yōu)點####

MVC
MVC
MVP
MVP

上面兩張圖分別是MVC和MVP架構(gòu)圖听哭。相信許多和我一樣嘗試去學(xué)習和了解MVP架構(gòu)的同學(xué)對這兩圖(或類似的圖)并不陌生。

結(jié)構(gòu)更加清晰

我們回過頭再去看MVCActivity 的實現(xiàn)塘雳,暫且將我們對Http請求的封裝歸結(jié)為Model (M)陆盘,那么剩下的就只有Activity了,而這個Activity即負責實現(xiàn)視圖邏輯败明,又需要實現(xiàn)部分業(yè)務(wù)邏輯隘马,也就是說他既是Controller(C) 又是 View(V)。V和C的劃分完全不清晰妻顶;因此酸员,傳統(tǒng)的代碼結(jié)構(gòu)只能勉強稱為MV 或者是MC,如果算上xml 的布局文件讳嘱,才能牽強的稱為MVC 結(jié)構(gòu)幔嗦。

而MVP 就不同了,Model呢燥,View崭添,Presenter各司其職寓娩,互相搭配叛氨,實現(xiàn)了解耦,完全解放了Activity(或者是Fragment)棘伴。這就是MVP 的優(yōu)勢寞埠,代碼結(jié)構(gòu)更加清晰『缚洌可以這樣說仁连,同一個模塊的實現(xiàn),甚至允許幾個人分工完成;假設(shè)有一個非常復(fù)雜的Activity饭冬,如果使用MVP 的模式開發(fā)使鹅;那么這個時候,定義好MVP的接口之后昌抠,就可以有人專門去做Model患朱,另一個人專門去做View;再由一個人寫Presenter的代碼炊苫,當然這需要極強的代碼規(guī)范和協(xié)作能力裁厅;但這在傳統(tǒng)的MVC模式中根本是無法想象的,所有的東西都在一個類里侨艾,兩個人一起改执虹,有了沖突怎么玩/(ㄒoㄒ)/~~。

需求變更唠梨,不再是噩夢

假設(shè)現(xiàn)在有新的需求袋励,產(chǎn)品經(jīng)理認為下載失敗后只有一個Toast提示太單調(diào)了(而且用戶有可能錯過了這Toast的顯示,而誤以為APP失去了響應(yīng))当叭,因此插龄,現(xiàn)在希望在下載失敗后彈出一個Dialog,可以重試下載任務(wù)科展。是想均牢,如果代碼使用傳統(tǒng)的MVC 結(jié)構(gòu),恰巧這個代碼不是你寫的才睹,或者說就是你寫的徘跪,但是你已經(jīng)忘記了具體的邏輯;那么為了實現(xiàn)這個需求你又得去重新捋一遍邏輯琅攘,到某個類的xxx行進行修改垮庐;但是如果使用MVP就不同了View接口已經(jīng)定義好了showFailToast就是用來顯示錯誤提示的;因此即便代碼不是你寫的坞琴,你都可以很快的找到哨查,應(yīng)該去哪里改;而省去很多時間剧辐。

更容易寫單元測試

這個就不展開說了寒亥,總之寫過單元測試的人應(yīng)該都有這樣的體會。

缺點####

MVP這么好荧关,也不是沒有缺點溉奕。

arch.png

如圖中所示,使用MVP 架構(gòu)之后忍啤,多出了許多類加勤;這是必然的;每一個View(Activity或Fragment)都至少需要各自的Model、Presenter和View接口鳄梅,在加上他們各自的實現(xiàn)叠国,也就是說每一個頁面都會有6個java文件(算上Fragment或Activity,因為他就是View的實現(xiàn))戴尸,這樣一個稍有點規(guī)模的APP煎饼,類就會變得異常的多,而每一個類的加載又會消耗資源校赤;因此吆玖,相較于MVC,這算是MVP最大的缺點了吧马篮。

當然沾乘,對于這個問題我們可以通過泛型參數(shù)、抽象父類的方式浑测,將一些公用的Model及Presenter抽象出來翅阵。這應(yīng)該就是使用MVP架構(gòu)的精髓了。

最后##

個人感覺迁央,使用MVP 架構(gòu)是利大于弊的掷匠;隨著項目規(guī)模的增加,代碼邏輯的清晰才是最重要的事情岖圈。況且Google官方也出推出了一系列關(guān)于MVP的使用demo讹语。因此,這也是官方提倡大家使用的蜂科。凡事顽决,有利必有弊;類數(shù)目的增長是無法避免的事情导匣,因此如何使用泛型和抽象優(yōu)化MVP 的結(jié)構(gòu)就變成了我們用好MVP的關(guān)鍵了才菠。

當然,我們不能為了MVP而去MVP贡定,如果項目結(jié)構(gòu)不是很龐大赋访,業(yè)務(wù)不是很復(fù)雜;那么傳統(tǒng)的MVC 架構(gòu)足以缓待,而且也方便蚓耽!


年前的最后一個工作日了,我居然寫了一篇學(xué)習筆記命斧;今天一定是上了假的班兒田晚!明天回家過年,O(∩_∩)O哈哈哈~国葬!每一個人,新年快樂!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末汇四,一起剝皮案震驚了整個濱河市接奈,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌通孽,老刑警劉巖序宦,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異背苦,居然都是意外死亡互捌,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進店門行剂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來秕噪,“玉大人,你說我怎么就攤上這事厚宰‰缃恚” “怎么了?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵铲觉,是天一觀的道長澈蝙。 經(jīng)常有香客問我,道長撵幽,這世上最難降的妖魔是什么灯荧? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮盐杂,結(jié)果婚禮上漏麦,老公的妹妹穿的比我還像新娘。我一直安慰自己况褪,他們只是感情好撕贞,可當我...
    茶點故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著测垛,像睡著了一般捏膨。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上食侮,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天号涯,我揣著相機與錄音,去河邊找鬼锯七。 笑死链快,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的眉尸。 我是一名探鬼主播域蜗,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼巨双,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了霉祸?” 一聲冷哼從身側(cè)響起筑累,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎丝蹭,沒想到半個月后慢宗,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡奔穿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年镜沽,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片贱田。...
    茶點故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡缅茉,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出湘换,到底是詐尸還是另有隱情宾舅,我是刑警寧澤,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布彩倚,位于F島的核電站筹我,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏帆离。R本人自食惡果不足惜蔬蕊,卻給世界環(huán)境...
    茶點故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望哥谷。 院中可真熱鬧岸夯,春花似錦、人聲如沸们妥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽监婶。三九已至旅赢,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間惑惶,已是汗流浹背煮盼。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留带污,地道東北人僵控。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像鱼冀,于是被迫代替她去往敵國和親报破。 傳聞我的和親對象是個殘疾皇子悠就,可洞房花燭夜當晚...
    茶點故事閱讀 42,834評論 2 345

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