記得第一次接觸MVP開發是上大學的時候,當時看了數十篇關於MVP的文章,這裏不得不吐槽一下國內技術帖子的質量真是參次不齊啊。看完以後一直懵懵懂懂的,總覺有幾處關鍵的地方沒搞清可是文章中卻一帶而過了,好比:android
抱着這些問題,我本身摸索着構建出了一套個性化風格MVP架構,使用過程當中也優化了幾回,現在一年多過去了再看這套架構也就算是個能用吧,因此決定新的架構優化。小程序
本文講述了MVP的核心概念和如何從最初的乞丐版MVP架構一步步升級到平民版MVP架構,時尚版MVP架構,以及即將開始更新的旗艦版MVP架構,爲了保證思路清晰,文中包含大量代碼與文字,跟着文中的例子即可寫出一個完整的MVP架構。api
其實咱們平常開發中的Activity,Fragment和XML界面就至關因而一個 MVC 的架構模式,Activity中不只要處理各類 UI 操做還要請求數據以及解析。安全
這種開發方式的缺點就是業務量大的時候一個Activity 文件分分鐘飆到上千行代碼,想要改一處業務邏輯光是去找就要費半天勁,並且有點地方邏輯處理是同樣的無奈是不一樣的 Activity 就沒辦法很好的寫成通用方法。bash
那 MVP 爲啥好用呢?markdown
MVP 模式將Activity 中的業務邏輯所有分離出來,讓Activity 只作 UI 邏輯的處理,全部跟Android API無關的業務邏輯由 Presenter 層來完成。網絡
將業務處理分離出來後最明顯的好處就是管理方便,可是缺點就是增長了代碼量。架構
在MVP 架構中跟MVC相似的是一樣也分爲三層。異步
Activity 和Fragment 視爲View層,負責處理 UI。ide
Presenter 爲業務處理層,既能調用UI邏輯,又能請求數據,該層爲純Java類,不涉及任何Android API。
Model 層中包含着具體的數據請求,數據源。
三層之間調用順序爲view->presenter->model,爲了調用安全着想不可反向調用!不可跨級調用!
那Model 層如何反饋給Presenter 層的呢?Presenter 又是如何操控View 層呢?看圖!
上圖中說明了低層的不會直接給上一層作反饋,而是經過 View 、 Callback 爲上級作出了反饋,這樣就解決了請求數據與更新界面的異步操做。上圖中 View 和 Callback 都是以接口的形式存在的,其中 View 是經典 MVP 架構中定義的,Callback 是我本身加的。
View 中定義了 Activity 的具體操做,主要是些將請求到的數據在界面中更新之類的。
Callback 中定義了請求數據時反饋的各類狀態:成功、失敗、異常等。
下面咱們用 MVP 模式構造一個簡易模擬請求網絡的小程序。效果圖以下:
由於是模擬網絡數據請求,因此有三個請求數據的按鈕分別對應成功、失敗、異常三種不一樣的反饋狀態。
下面是Demo中的Java文件目錄:
Callback 接口是Model層給Presenter層反饋請求信息的傳遞載體,因此須要在Callback中定義數據請求的各類反饋狀態:
public interface MvpCallback {
/**
* 數據請求成功
* @param data 請求到的數據
*/
void onSuccess(String data);
/**
* 使用網絡API接口請求方式時,雖然已經請求成功可是由
* 於{@code msg}的緣由沒法正常返回數據。
*/
void onFailure(String msg);
/**
* 請求數據失敗,指在請求網絡API接口請求方式時,出現沒法聯網、
* 缺乏權限,內存泄露等緣由致使沒法鏈接到請求數據源。
*/
void onError();
/**
* 當請求數據結束時,不管請求結果是成功,失敗或是拋出異常都會執行此方法給用戶作處理,一般作網絡
* 請求時能夠在此處隱藏「正在加載」的等待控件
*/
void onComplete();
}
複製代碼
Model 類中定了具體的網絡請求操做。爲模擬真實的網絡請求,利用postDelayed
方法模擬耗時操做,經過判斷請求參數反饋不一樣的請求狀態:
public class MvpModel { /** * 獲取網絡接口數據 * @param param 請求參數 * @param callback 數據回調接口 */ public static void getNetData(final String param, final MvpCallback callback){ // 利用postDelayed方法模擬網絡請求數據的耗時操做 new Handler().postDelayed(new Runnable() { @Override public void run() { switch (param){ case "normal": callback.onSuccess("根據參數"+param+"的請求網絡數據成功"); break; case "failure": callback.onFailure("請求失敗:參數有誤"); break; case "error": callback.onError(); break; } callback.onComplete(); } },2000); } } 複製代碼
View接口是Activity與Presenter層的中間層,它的做用是根據具體業務的須要,爲Presenter提供調用Activity中具體UI邏輯操做的方法。
public interface MvpView {
/**
* 顯示正在加載進度框
*/
void showLoading();
/**
* 隱藏正在加載進度框
*/
void hideLoading();
/**
* 當數據請求成功後,調用此接口顯示數據
* @param data 數據源
*/
void showData(String data);
/**
* 當數據請求失敗後,調用此接口提示
* @param msg 失敗緣由
*/
void showFailureMessage(String msg);
/**
* 當數據請求異常,調用此接口提示
*/
void showErrorMessage();
}
複製代碼
Presenter類是具體的邏輯業務處理類,該類爲純Java類,不包含任何Android API,負責請求數據,並對數據請求的反饋進行處理。
Presenter類的構造方法中有一個View接口的參數,是爲了可以經過View接口通知Activity進行更新界面等操做。
public class MvpPresenter { // View接口 private MvpView mView; public MvpPresenter(MvpView view){ this.mView = view; } /** * 獲取網絡數據 * @param params 參數 */ public void getData(String params){ //顯示正在加載進度條 mView.showLoading(); // 調用Model請求數據 MvpModel.getNetData(params, new MvpCallback() { @Override public void onSuccess(String data) { //調用view接口顯示數據 mView.showData(data); } @Override public void onFailure(String msg) { //調用view接口提示失敗信息 mView.showFailureMessage(msg); } @Override public void onError() { //調用view接口提示請求異常 mView.showErrorMessage(); } @Override public void onComplete() { // 隱藏正在加載進度條 mView.hideLoading(); } }); } } 複製代碼
沒什麼好說的,直接上代碼:
<?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:padding="16dp" android:orientation="vertical" tools:context="com.jessewu.mvpdemo.MainActivity"> <TextView android:id="@+id/text" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:text="點擊按鈕獲取網絡數據"/> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:text="獲取數據【成功】" android:onClick="getData" /> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:text="獲取數據【失敗】" android:onClick="getDataForFailure" /> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:text="獲取數據【異常】" android:onClick="getDataForError" /> </LinearLayout> 複製代碼
在Activity代碼中須要強調的是若是想要調用Presenter就要先實現Presenter須要的對應的View接口。
public class MainActivity extends AppCompatActivity implements MvpView { //進度條 ProgressDialog progressDialog; TextView text; MvpPresenter presenter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); text = (TextView)findViewById(R.id.text); // 初始化進度條 progressDialog = new ProgressDialog(this); progressDialog.setCancelable(false); progressDialog.setMessage("正在加載數據"); //初始化Presenter presenter = new MvpPresenter(this); } // button 點擊事件調用方法 public void getData(View view){ presenter.getData("normal"); } // button 點擊事件調用方法 public void getDataForFailure(View view){ presenter.getData("failure"); } // button 點擊事件調用方法 public void getDataForError(View view){ presenter.getData("error"); } @Override public void showLoading() { if (!progressDialog.isShowing()) { progressDialog.show(); } } @Override public void hideLoading() { if (progressDialog.isShowing()) { progressDialog.dismiss(); } } @Override public void showData(String data) { text.setText(data); } @Override public void showFailureMessage(String msg) { Toast.makeText(this, msg, Toast.LENGTH_SHORT).show(); text.setText(msg); } @Override public void showErrorMessage() { Toast.makeText(this, "網絡請求數據出現異常", Toast.LENGTH_SHORT).show(); text.setText("網絡請求數據出現異常"); } } 複製代碼
至此,已經完整的實現了一個簡易的MVP架構。
注意!以上代碼中還存在很大的問題,不能用到實際開發中,下面會討論目前存在的問題以及如何優化。
由於上節中乞丐版MVP Demo的代碼只實現了一個Activity的請求操做,容易出現一個概念的混淆:
每一個Activity都須要有與它對應的一套MVP(Model,View,Presenter)嗎?
答案確定是否認的!
首先不須要數據請求的Activity固然就一樣不須要MVP輔助。與其餘Activity中存在相同邏輯的Activity,就不須要重複生成對應的MVP。可是這個存在相同邏輯的定義,不一樣的場景有不一樣的說法:
場景1中Activity A和Activity C都只有一個「買東西」的邏輯,屬於典型的邏輯相同,因此Activity C就能夠直接用Activity A寫好的MVP無需再作任何處理。
場景2和場景3的邏輯相似,都屬於一個業務邏輯中包含另一個能夠單獨存在的業務邏輯,這種狀況採用繼承的方法便可:
場景4中Activity C想要同時調用獨立服務於Activity A 和 Activity B的業務邏輯,只須要將兩個業務邏輯對應的Presenter分別實例化並調用業務方法便可:
private PresenterA presenterA; private PresenterB presenterB; ... ... private void getData(){ presenterA.getData(); presenterB.getData(); } 複製代碼
不要忘了實現兩個Presenter對應的View:
public class ActivityC extends Activity implements ViewA,ViewB{
...
}
複製代碼
場景5屬於場景3與場景4的結合體,一樣須要先把A和B的業務邏輯拆分開,而後同時調用,這裏就不舉例子了。
經過上面一攬子場景的分析,得出的第一個結論就是MVP的結構太過於繁重,因此爲了不多寫重複代碼和往後須要進行無心義的修改,在開發前必定要設計好邏輯調用圖,這樣才能事半功倍。
對於上面經典的經過業務邏輯繼承實現包含重複邏輯的方法,其實也能夠在一個Presenter中寫好完整的邏輯方法,對於不一樣的Activity須要哪一個業務邏輯方法就調用哪一個,這樣豈不就簡單多了。可是從架構設計角度看這種作法是不嚴謹的,可能存在漏洞,因此爲保持軟件架構的健壯仍是不要偷懶的好。
以前說過乞丐版MVP架構模式中還存在不少問題不能應用到實際的開發中,大概存在的問題有:
針對這些問題咱們須要進一步優化,單車變摩托,升級爲能夠在實際開發中使用的平民版MVP架構。
舉一個例子,在上述乞丐版MVP架構中的應用請求網絡數據時須要等待後臺反饋數據後更新界面,可是在請求過程當中當前Activity忽然由於某種緣由被銷燬,Presenter收到後臺反饋並調用View接口處理UI邏輯時因爲Activity已經被銷燬,就會引起空指針異常。
想要避免這種狀況的發生就須要每次調用View前都知道宿主Activity的生命狀態。
以前是在Presenter的構造方法中獲得View接口的引用,如今咱們須要修改Presenter引用View接口的方式讓View接口與宿主Activity共存亡:
public class MvpPresenter { // View接口 private MvpView mView; public MvpPresenter(){ //構造方法中再也不須要View參數 }  /** * 綁定view,通常在初始化中調用該方法 */ public void attachView(MvpView mvpView) { this.mView= mvpView; } /** * 斷開view,通常在onDestroy中調用 */ public void detachView() { this.mView= null; } /** * 是否與View創建鏈接 * 每次調用業務請求的時候都要出先調用方法檢查是否與View創建鏈接 */ public boolean isViewAttached(){ return mView!= null; } /** * 獲取網絡數據 * @param params 參數 */ public void getData(String params){ //顯示正在加載進度條 mView.showLoading(); // 調用Model請求數據 MvpModel.getNetData(params, new MvpCallback() { @Override public void onSuccess(String data) { //調用view接口顯示數據 if(isViewAttached()){ mView.showData(data); } } @Override public void onFailure(String msg) { //調用view接口提示失敗信息 if(isViewAttached()){ mView.showFailureMessage(msg); } } @Override public void onError() { //調用view接口提示請求異常 if(isViewAttached()){ mView.showErrorMessage(); } } @Override public void onComplete() { // 隱藏正在加載進度條 if(isViewAttached()){ mView.hideLoading(); } } }); } } 複製代碼
上面Presenter代碼中比以前增長了三個方法:
attachView()
綁定View引用。detachView
斷開View引用。isViewAttached()
判斷View引用是否存在。其中attachView()
和detachView()
是爲Activity準備的,isViewAttached()
做用是Presenter內部每次調用View接口中的方法是判斷View 的引用是否存在。
把綁定View的方法寫到Activity的生命週期中:
public class MainActivity extends Activity implements MvpView{ MvpPresenter presenter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //初始化Presenter presenter = new MvpPresenter(); // 綁定View引用 presenter.attachView(this); } @Override protected void onDestroy() { super.onDestroy(); // 斷開View引用 presenter.detachView(); } } 複製代碼
寫到這裏,相信大多數人都會驚訝於MVP模式代碼量的巨大,冗餘代碼實在太多,因此接下須要爲MVP中全部單元都設計一個頂級父類來減小重複的冗餘代碼。一樣的道理,咱們也爲Activity設計一個父類方便與MVP架構更完美的結合。最後將全部父類單獨分到一個base
包中供外界繼承調用。
在乞丐版中Callback接口中的onSuccess()
方法須要根據請求數據類型的不一樣設置爲不一樣類型的參數,因此每當有新的數據類型都須要新建一個Callback,解決方法是引入泛型的概念,用調用者去定義具體想要接收的數據類型:
public interface Callback<T> {
/**
* 數據請求成功
* @param data 請求到的數據
*/
void onSuccess(T data);
/**
* 使用網絡API接口請求方式時,雖然已經請求成功可是由
* 於{@code msg}的緣由沒法正常返回數據。
*/
void onFailure(String msg);
/**
* 請求數據失敗,指在請求網絡API接口請求方式時,出現沒法聯網、
* 缺乏權限,內存泄露等緣由致使沒法鏈接到請求數據源。
*/
void onError();
/**
* 當請求數據結束時,不管請求結果是成功,失敗或是拋出異常都會執行此方法給用戶作處理,一般作網絡
* 請求時能夠在此處隱藏「正在加載」的等待控件
*/
void onComplete();
}
複製代碼
View接口中定義Activity的UI邏輯。由於有不少方法幾乎在每一個Activity中都會用到,例如顯示和隱藏正在加載進度條,顯示Toast提示等,索性將這些方法變成通用的:
public interface BaseView { /** * 顯示正在加載view */ void showLoading(); /** * 關閉正在加載view */ void hideLoading(); /** * 顯示提示 * @param msg */ void showToast(String msg); /** * 顯示請求錯誤提示 */ void showErr(); /** * 獲取上下文 * @return 上下文 */ Context getContext(); } 複製代碼
Presenter中可共用的代碼就是對View引用的方法了,值得注意的是,上面已經定義好了BaseView
,因此咱們但願Presenter中持有的View都是BaseView
的子類,這裏一樣須要泛型來約束:
public class BasePresenter<V extends BaseView> { /** * 綁定的view */ private V mvpView; /** * 綁定view,通常在初始化中調用該方法 */ @Override public void attachView(V mvpView) { this.mvpView = mvpView; } /** * 斷開view,通常在onDestroy中調用 */ @Override public void detachView() { this.mvpView = null; } /** * 是否與View創建鏈接 * 每次調用業務請求的時候都要出先調用方法檢查是否與View創建鏈接 */ public boolean isViewAttached(){ return mvpView != null; } /** * 獲取鏈接的view */ public V getView(){ return mvpView; } 複製代碼
BaseActivity主要是負責實現 BaseView 中通用的UI邏輯方法,如此這些通用的方法就不用每一個Activity都要去實現一遍了。
public abstract class BaseActivity extends Activity implements BaseView { private ProgressDialog mProgressDialog; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mProgressDialog = new ProgressDialog(this); mProgressDialog.setCancelable(false); } @Override public void showLoading() { if (!mProgressDialog.isShowing()) { mProgressDialog.show(); } } @Override public void hideLoading() { if (mProgressDialog.isShowing()) { mProgressDialog.dismiss(); } } @Override public void showToast(String msg) { Toast.makeText(this, msg, Toast.LENGTH_SHORT).show(); } @Override public void showErr() { showToast(getResources().getString(R.string.api_error_msg)); } @Override public Context getContext() { return BaseActivity.this; } 複製代碼
封裝好了base層咱們的平民版MVP架構就完成了,下面再來實現一遍以前用乞丐版MVP實現的應用。
public class MvpModel { /** * 獲取網絡接口數據 * @param param 請求參數 * @param callback 數據回調接口 */ public static void getNetData(final String param, final MvpCallback<String> callback){ // 利用postDelayed方法模擬網絡請求數據的耗時操做 new Handler().postDelayed(new Runnable() { @Override public void run() { switch (param){ case "normal": callback.onSuccess("根據參數"+param+"的請求網絡數據成功"); break; case "failure": callback.onFailure("請求失敗:參數有誤"); break; case "error": callback.onError(); break; } callback.onComplete(); } },2000); } } 複製代碼
public interface MvpView extends BaseView{
/**
* 當數據請求成功後,調用此接口顯示數據
* @param data 數據源
*/
void showData(String data);
}
複製代碼
public class MvpPresenter extends BasePresenter<MvpView > { /** * 獲取網絡數據 * @param params 參數 */ public void getData(String params){ if (!isViewAttached()){ //若是沒有View引用就不加載數據 return; } //顯示正在加載進度條 getView().showLoading(); // 調用Model請求數據 MvpModel.getNetData(params, new MvpCallback()<String> { @Override public void onSuccess(String data) { //調用view接口顯示數據 if(isViewAttached()){ getView().showData(data); } } @Override public void onFailure(String msg) { //調用view接口提示失敗信息 if(isViewAttached()){ getView().showToast(msg); } } @Override public void onError() { //調用view接口提示請求異常 if(isViewAttached()){ getView().showErr(); } } @Override public void onComplete() { // 隱藏正在加載進度條 if(isViewAttached()){ getView().hideLoading(); } } }); } } 複製代碼
public class MainActivity extends BaseActivity implements MvpView { TextView text; MvpPresenter presenter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); text = (TextView)findViewById(R.id.text); //初始化Presenter presenter = new MvpPresenter(); presenter.attachView(this); } @Override protected void onDestroy() { super.onDestroy(); //斷開View引用 presenter.detachView(); } @Override public void showData(String data) { text.setText(data); } // button 點擊事件調用方法 public void getData(View view){ presenter.getData("normal"); } // button 點擊事件調用方法 public void getDataForFailure(View view){ presenter.getData("failure"); } // button 點擊事件調用方法 public void getDataForError(View view){ presenter.getData("error"); } } 複製代碼
平常開發中,並非全部的UI處理都在Activity中進行,Fragment也是其中很重要的一員,那麼如何將Fragment結合到MVP中呢?
實現BaseFragement作法跟BaseActivity很相似,須要注意一下Fragement與宿主Activity的連接狀況就能夠。
public abstract class BaseFragment extends Fragment implements BaseView { public abstract int getContentViewId(); protected abstract void initAllMembersView(Bundle savedInstanceState); protected Context mContext; protected View mRootView; @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { mRootView = inflater.inflate(getContentViewId(), container, false); this.mContext = getActivity(); initAllMembersView(savedInstanceState); return mRootView; } @Override public void showLoading() { checkActivityAttached(); ((BaseFragmentActivity) mContext).showLoading(); } @Override public void showLoading(String msg) { checkActivityAttached(); ((BaseFragmentActivity) mContext).showLoading(msg); } @Override public void hideLoading() { checkActivityAttached(); ((BaseFragmentActivity) mContext).hideLoading(); } @Override public void showToast(String msg) { checkActivityAttached(); ((BaseFragmentActivity) mContext).showToast(msg); } @Override public void showErr() { checkActivityAttached(); ((BaseFragmentActivity) mContext).showErr(); } protected boolean isAttachedContext(){ return getActivity() != null; } /** * 檢查activity鏈接狀況 */ public void checkActivityAttached() { if (getActivity() == null) { throw new ActivityNotAttachedException(); } } public static class ActivityNotAttachedException extends RuntimeException { public ActivityNotAttachedException() { super("Fragment has disconnected from Activity ! - -."); } } } 複製代碼
在從乞丐版MVP架構優化成平民版MVP架構的過程當中,幾乎每一個單元都作了很大優化並封裝到了base層,可是惟獨Model層沒什麼變化。因此,時尚版MVP架構的優化主要就是對Model層的優化。
Model層相比其餘單元來講比較特殊,由於它們更像一個總體,只是單純的幫上層拿數據而已。再就是MVP的理念是讓業務邏輯互相獨立,這就致使每一個的網絡請求也被獨立成了單個Model,不光不必這麼作並且找起來賊麻煩,因此時尚版MVP架構中Model層被總體封裝成了龐大且獨立單一模塊。
優化以後的Model層是一個龐大並且獨立的模塊,對外提供統一的請求數據方法與請求規則,這樣作的好處有不少:
下篇會完善時尚版MVP架構,以及最新的旗艦版MVP架構設計,敬請期待~