前言
在日常開發APP 的過程中,隨著業務的擴充套件,規模的變化。我們的程式碼規模也會逐漸變得龐大,每一個類裡的程式碼也會逐漸增多。尤其是Activity和Fragment ,由於Context 的存在,基本上所有對檢視的操作我們只能在Activity和Fragment中完成;即便是對某些邏輯進行封裝,Activity和Fragment 依舊會顯得過於臃腫。因此,我們需要換一種思路去寫程式碼,這個時候MVP模式就應用而生了!那麼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的區別;下面就其各自的優缺點再做一下
總結;當然,這裡的優缺點只是相對而言。
優點
上面兩張圖分別是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這麼好,也不是沒有缺點。
如圖中所示,使用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哈哈哈~!每一個人,新年快樂!