Android MVP 十分鐘入門!

IAM四十二發表於2017-01-24

前言

在日常開發APP 的過程中,隨著業務的擴充套件,規模的變化。我們的程式碼規模也會逐漸變得龐大,每一個類裡的程式碼也會逐漸增多。尤其是Activity和Fragment ,由於Context 的存在,基本上所有對檢視的操作我們只能在Activity和Fragment中完成;即便是對某些邏輯進行封裝,Activity和Fragment 依舊會顯得過於臃腫。因此,我們需要換一種思路去寫程式碼,這個時候MVP模式就應用而生了!那麼MVP 怎麼用呢,下面就來說一說。

假設你現在如要實現下圖中的功能:

Android MVP 十分鐘入門!

這個需求很簡單,就是點選按鈕,下載一張圖片,顯示下載進度;下載完成後,在ImageView中顯示這張圖片。
下面我們就分別用傳統的方式(也就是所謂的MVC)和MVP 模式分別取實現這個功能。然後分析一下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,然後開始下載任務(這裡HttpUtil 內部簡單封裝了OKHttp 的非同步GET請求,實現下載檔案儲存到本地的功能,實現細節在此不做深入探討,有興趣的同學可以檢視原始碼),然後將請求結果通過Handler返回,在handleMessage中根據返回資料的資訊做出不同的UI 處理;下載成功時在ImageView中顯示圖片,下載失敗時Toast提示。

可以發現,在這種情況之前,Activity的任務十分繁重,既要負責下載任務的具體實施,還要根據下載進行再次的邏輯判斷,才能去更新UI。這裡只是一個簡單的任務,你可能覺得無所謂,但是實際開發中,一個Activity中有許多的互動事件,這個時候Activity的程式碼就顯得特別的龐大;一旦需求變更或出現bug,那簡直就是噩夢一場。

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

  • 他負責發起處理和使用者互動的內容,但又不負責具體的實現;
  • 需要顯示什麼,不顯示什麼,什麼東西顯示多少,有個東西可以直接告訴他,
  • Activity不再做複雜的邏輯處理;

具體到上面的demo裡就是,Activity負責發起下載任務,但是不負責具體實現;什麼時候顯示ProgressDialog,顯示多少?什麼時候提示錯誤資訊,這一切都希望有個東西能直接告訴Activity,而不再是在Activity裡再做判斷。怎樣才能做到呢?那就得靠MVP 了。

MVP

MVP 模式所做的事情很簡單,就是將業務邏輯和檢視邏輯抽象到介面中。

怎麼理解呢,我們就根據此次要實現的下載功能,用程式碼說話。

定義Model,View,Presenter 介面

Model Interface

Model 介面定義所有需要實現的業務邏輯,在我們的下載任務中,業務邏輯只有一個,就是下載;因此Model 介面可以這麼定義 :

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

View Interface

View 介面定義所有需要實現的檢視邏輯,在我們的下載任務中,檢視邏輯包括

  • 顯示ProgressDialog;
  • 顯示Dialog具體進度;
  • 顯示具體的View(設定圖片);
  • 顯示錯誤資訊(Toast提示)

因此View介面可以這麼定義:

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

    /**
     * 設定進度條進度
     * @param progress
     */
    void setProcessProgress(int progress);

    /**
     * 根據資料設定view
     * @param result
     */
    void setView(String result);

    /**
     * 設定請求失敗時的view
     */
    void showFailToast();
}複製程式碼

Presenter Interface

Presenter 介面作為連線Model和View的中間橋樑,需要將二者連線起來,因此他需要完成以下工作:

  • 執行下載任務
  • 下載成功返回下載結果
  • 下載過程返回下載進度
  • 下載失敗回撥

因此,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 具體實現

上面實現了,各個介面的定義,下面來看看他們具體的實現:

Model 具體實現

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的工作就是完成具體的業務操作,網路請求,持久化資料增刪改查等任務。同時Model中又不會包含任何View。
這裡Model的具體實現很簡單,將Http任務的結果返回到Handler當中,而在Handler中的實現又是由Presenter完成。
那麼Presenter介面又是怎樣實現的呢?趕緊來看看

Presenter 具體實現

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的構造方法中,同時例項化了Model和View,這樣Presenter中就同時包含了兩者;
這樣;在Presenter具體實現中,業務相關的操作由Model去完成(例如download),檢視相關的操作由View去完成
(如setView等)
。Presenter 作為橋樑的作用就這樣體現出來了,巧妙的將View和Model的具體實現連線了起來。

View具體實現

最後再看一下View介面的具體實現,也就是Activity的實現:

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();
    }
}複製程式碼

在點下按鈕執行開始下載任務的時候,View(Activity)中沒有具體的實現,只是呼叫了Presenter中的download方法,而Presenter中的download又會去呼叫Model的download方法,Model又會在根據具體邏輯(在這裡就是Http請求)的狀態去呼叫Presenter中的方法,例如我們在handleMessage方法中,呼叫mIDowndownPresenter.downloadProgress(percent)時,就會去呼叫Presenter的具體實現

    @Override
    public void downloadProgress(int progress) {
        mIDownloadView.setProcessProgress(progress);
    }複製程式碼

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


    @Override
    public void setProcessProgress(int progress) {
        progressDialog.setProgress(progress);
    }複製程式碼

我們為progressDialog 設定進度。

至此,我們就通過MVP 的模式實現了我們之前所設想的Activity

  • Button的click方法負責發起下載任務,但又不負責具體實現,而是由Presenter轉接給Model去實現
  • Activity 什麼時候顯示ProgressDialog,什麼時候顯示Toast直接由Presenter告訴他,他只做一個View想做的事情
  • Activity裡沒有任何邏輯處理,所有的邏輯判斷都在Model中完成了。

這就是MVP !!!

MVC VS MVP

通過上面的兩種實現方案,相信每個人都已經理解了MVC和MVP的區別;下面就其各自的優缺點再做一下
總結;當然,這裡的優缺點只是相對而言

優點

Android MVP 十分鐘入門!
MVC

Android MVP 十分鐘入門!
MVP

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

結構更加清晰*

我們回過頭再去看MVCActivity 的實現,暫且將我們對Http請求的封裝歸結為Model(M),那麼剩下的就只有Activity了,而這個Activity即實現檢視邏輯,又需要實現部分業務邏輯,也就是說他既是Controller(C)又是View(V)。V和C的劃分完全不清晰;因此,傳統的程式碼結構只能勉強稱為MV 或者是MC,如果算上xml 的佈局檔案,才能牽強的稱為MVC 結構。

而MVP 就不同了,Model,View,Presenter各司其職,互相搭配,實現瞭解耦,完全解放了Activity(或者是Fragment)。這就是MVP 的優勢,程式碼結構更加清晰。可以這樣說,同一個模組的實現,甚至允許幾個人分工完成;假設有一個非常複雜的Activity,如果使用MVP 的模式開發;那麼這個時候,定義好MVP的介面之後,就可以有人專門去做Model,另一個人專門去做View;再由一個人寫Presenter的程式碼,當然這需要極強的程式碼規範和協作能力;但這在傳統的MVC模式中根本是無法想象的,所有的東西都在一個類裡,兩個人一起改,有了衝突怎麼玩/(ㄒoㄒ)/~~。

需求變更,不再是噩夢

假設現在有新的需求,產品經理認為下載失敗後只有一個Toast提示太單調了(而且使用者有可能錯過了這Toast的顯示,而誤以為APP失去了響應),因此,現在希望在下載失敗後彈出一個Dialog,可以重試下載任務。是想,如果程式碼使用傳統的MVC 結構,恰巧這個程式碼不是你寫的,或者說就是你寫的,但是你已經忘記了具體的邏輯;那麼為了實現這個需求你又得去重新捋一遍邏輯,到某個類的xxx行進行修改;但是如果使用MVP就不同了View介面已經定義好了showFailToast就是用來顯示錯誤提示的;因此即便程式碼不是你寫的,你都可以很快的找到,應該去哪裡改;而省去很多時間。

更容易寫單元測試

這個就不展開說了,總之寫過單元測試的人應該都有這樣的體會。

缺點

MVP這麼好,也不是沒有缺點。

Android MVP 十分鐘入門!

如圖中所示,使用MVP 架構之後,多出了許多類;這是必然的;每一個View(Activity或Fragment)都至少需要各自的Model、Presenter和View介面,在加上他們各自的實現,也就是說每一個頁面都會有6個java檔案(算上Fragment或Activity,因為他就是View的實現),這樣一個稍有點規模的APP,類就會變得異常的多,而每一個類的載入又會消耗資源;因此,相較於MVC,這算是MVP最大的缺點了吧。

當然,對於這個問題我們可以通過泛型引數、抽象父類的方式,將一些公用的Model及Presenter抽象出來。這應該就是使用MVP架構的精髓了。

最後

個人感覺,使用MVP 架構是利大於弊的;隨著專案規模的增加,程式碼邏輯的清晰才是最重要的事情。況且Google官方也出推出了一系列關於MVP的使用demo
因此,這也是官方提倡大家使用的。凡事,有利必有弊;類數目的增長是無法避免的事情,因此如何使用泛型和抽象優化MVP 的結構就變成了我們用好
MVP的關鍵了。

當然,我們不能為了MVP而去MVP,如果專案結構不是很龐大,業務不是很複雜;那麼傳統的MVC 架構足以,而且也方便!


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

相關文章