Android:聊聊我所理解的MVP

寫在前面

最近冷靜了一段時間,復習復習之前學的東西。再加上陰陽師一直抽不到SSR,所以打副本的時候想了想畢設項目架構該怎么辦。

之前看很多開源軟件實現都是各種 MVP ,看起來很高大上,不過說實話,很早就了解 MVP 了,但一直很抗拒去學習,因為覺得模式或者架構類的東西屬于一種思想,并不是固定的寫法,而學習思想之前,必須要學會在引進這種思想之前是如何處理這些問題的。

也就是說,在學 MVP 之前,我得弄明白為啥會提出 MVP ,因為在 MVP 提出之前都是用 MVC 去處理的,所以我得學會 MVC ,當我能熟練的使用 MVC 的時候,再去學習 MVP ,這樣能很清楚的明白兩者之間的區別或者各自的優缺點,個人覺得這樣學起來還是比較好的,而不是盲目跟風,包括現在很多博客提到的 React NativeDagger2 等等都是一樣的道理。

現在我也來簡單聊一聊我自己所理解的 MVP ,不過只能算個入門吧。

不可少的介紹

關于 MVP 大家或多或少都知道一點,網上關于 MVP 的教程也很多,不過優質的就太少了。入門我只看了兩篇文章(泡網+鴻洋),文末都會有鏈接。

先上一個經典的圖:

C 和 P 的區別

先來看一下 MVPMVC 差別在哪?簡單一眼掃過,就是 CP 的差別。

1、先看 C

C就是 Controller ,控制器。負責從 View 讀取數據,控制用戶輸入,并向 Model 發送數據。簡單來說,就是起到一個溝通的作用,能 很大程度 上的解決 ModelView 的耦合問題。

換句話說就是,它是一個 ModelView 之間的橋梁,讓 ModelView 之間不再緊緊關聯。

比如 View 接收到了用戶輸入數據,先交給 ControllerController 再轉交給 Model ,反之亦然。

這就像小明喜歡隔壁班小紅,小明寫了一封情書需要通過隔壁班小王,才能交給小紅。

但是注意,我只是說能很大程度上解決,并不能徹底解決,也就是說小明如果發現了隔壁小王有問題,他仍然可以選擇直接把情書交給小紅。

2、再看 P

P就是 Presenter ,我翻譯成主持者。跟 C 類似,仍然是負責 ViewModel 之間的溝通。但是它徹底讓 ViewModel 不能直接溝通。如果想要溝通,就必須通過這個主持者來主持它們兩個應該干啥。

比如 View 接收到了用戶輸入數據,不能直接給 Model ,要交給 PresenterPresenter 再轉交給 Model ,反之亦然。

這就像我給主席寄了一個包裹,但這個包裹必須經過重重安檢,才能交到主席手上。

這就徹底斷了我跟主席……哦不對, ModelView 之間的聯系。

3、簡單區別

僅從目前來看, CP 都是為了解放 ModelView 之間的聯系,只不過 C 是很大程度上解決,但 P 是徹底讓它們兩斷了聯系。

換成技術術語來說就是一句話:

C讓 ModelView 做到 松散耦合 ,而 P 直接將它們 解耦

MVC 和 MVP 的區別

知道了各自簡單的作用,再來更深層次的理解 CP 在各自的 MV+X 中到底分別做了什么?

1、先看 MVC

從下圖中我們可以看到:

  • 用戶 Event (事件)會導致 Controller 改變 ModelView 或同時改變兩者。

  • 只要 Controller 改變了 Model 的數據或屬性,所有依賴的 View 都會自動更新。

  • 類似的,只要 Controller 改變了 ViewView 會從潛在的 Model 中獲取數據進行更新。

2、再看 MVP

從下圖中我們又能看到:

  • Presenter 中同時持有 View 以及 ModelInterface 引用,而 View 持有 Presenter 的實例。

  • 當某個 View 需要展示某些數據時,首先會調用 Presenter 的某個接口,然后 Presenter 會調用 Model 請求數據。

  • Model 數據加載成功后會調用 Presenter 的回調方法通知 Presenter 數據加載完畢,最后 Presenter 再調用 View 層接口展示加載后數據。

3、主要區別

MVC 中:

  • View 可以與 Model 直接交互;

  • Controller 可以被多個 View 共享;

  • Controller 可以決定顯示哪個 View

MVP 中:

  • View 不直接與 Model 交互;

  • PresenterView 通過接口來交互,更有利于添加單元測試;

  • 通常 ViewPresenter 是一對一的,但復雜的 View 可能綁定多個 Presenter 來處理;

  • Presenter 也可以直接進行 View 上的渲染。

經典案例

當然是那個經典的登錄案例,不過這里順帶學下畢設里幾個 MD 風格的開源庫。先來看一下運行的效果圖吧:

先分析

好了,動手之前先分析一下。

從上面內容我們知道, Presenter 是用來 ModelView 之間交互的。所以必須要持有它們各自的對象,根據需求一般都是用接口來實現。

而實現 View 層接口的一般都是 Activity (暫且這樣認為,后文還需要討論)。

當然如果想要 ActivityModel 進行交互,那么這個 Activity 中還必須有一個 Presenter 的實例,因為需要這個 Presenter 來進行交互嘛!

OK,把上面所有的東西捋一捋,數一數到底需要啥:

  • Model :負責存儲、檢索、操縱數據,一般都會一些封裝對 Bean 進行操作。

  • ModelInterface :這個不是必須的,但有時候如果幾個 Bean 之間有共性,可以抽一個接口出來。

  • View :暫且就認為是 Activity 。

  • ViewInterface :View 需要實現的接口,View 和 Presenter 也是通過它來進行交互。

  • Presenter :最重要的 View 和 Model 的橋梁,處理與用戶交互的負責邏輯,需要持有 View 和 Model 的接口對象。

雖然看起來東西確實變多了,但是結構看起來還是很清晰的,擴展起來也比較方便。

再動手

按照上面需要的東西,一步一步來:

1、先建一個 Bean

/**

  • @author xiarui 16/09/20
  • @description Person的Bean類 */ public class PersonBean { private String name ; private String pwd; //...省略 }</code></pre>

    2、再建立 Model Interface

    針對這個 Bean ,有注冊和登錄的功能,這里強行抽取一個 IPersonModel 接口出來,純屬為了展示用,意義不大:

    /**
  • @author xiarui 16/09/20
  • @description IPersonModel接口
  • @remark 接口其實不必實現 只是為了講解例子強行抽取的方法 */ public interface IPersonModel { //注冊賬號 boolean onRegister(String name, String pwd); //登錄賬號 boolean onLogin(String name, String pwd); }</code></pre>

    3、其次建立 Model

    實現了上一步建立的 Model Interface ,主要是對注冊和登錄方法的實現:

    **
  • @author xiarui 16/09/20
  • @description Model類 實現IPersonModel接口
  • @remark 接口其實不必實現 只是為了講解例子強行實現的 */ public class PersonModel implements IPersonModel {

    //簡單的存一下注冊的賬號 private Map<String, String> personMap = new HashMap<>();

    /**

    • 注冊賬號 存入集合 *
    • @param name 用戶名
    • @param pwd 密碼
    • @return true:注冊成功,false:注冊失敗 */ @Override public boolean onRegister(String name, String pwd) { if (!personMap.containsKey(name)) {

       personMap.put(name, pwd);
       return true;
      

      } return false; }

      /**

    • 登錄賬號 *
    • @param name 用戶名
    • @param pwd 密碼
    • @return true:登錄成功,false:登錄失敗 */ @Override public boolean onLogin(String name, String pwd) { return pwd.equals(personMap.get(name)); } }</code></pre>

      4、還需要 View Interface

      在這里我設定了五個方法,其中注冊/登錄成功與否分別建了兩個方法,原因后文再說:

      /**
  • @author xiarui 16/09/20
  • @description IPersonView接口 */ public interface IPersonView { boolean checkInputInfo(); //檢查輸入的合法性 void onRegisterSucceed(); //注冊成功 void onRegisterFaild(); //注冊失敗 void onLoginSucceed(); //登錄成功 void onLoginFaild(); //登錄失敗 }</code></pre>

    5、最重要的 Presenter

    再次強調, Presenter 是用來 ModelView 交互的,而它們各自都實現了接口,那我們只需保證 Presenter 持有這些接口即可:

    /**
  • @author xiarui 16/09/20
  • @description Person的Presenter類
  • @remark 必須要傳M和V 因為P需要控制M和V */ public class PersonPresenter {

    private IPersonModel mPersonModel; //Model接口 private IPersonView mPersonView; //View接口

    public PersonPresenter(IPersonView mPersonView) {

     mPersonModel = new PersonModel();
     this.mPersonView = mPersonView;
    

    }

    public void registerPerson(String name, String pwd) {

     boolean isRegister = mPersonModel.onRegister(name, pwd);
     //根據Model中的結果調用不同的方法進行UI展示
     if(isRegister){
         mPersonView.onRegisterSucceed();
     }else{
         mPersonView.onRegisterFaild();
     }
    

    }

    public void loginPerson(String name, String pwd) {

     boolean isLogin = mPersonModel.onLogin(name, pwd);
     //根據Model中的結果調用不同的方法進行UI展示
     if (isLogin) {
         mPersonView.onLoginSucceed();
     }else{
         mPersonView.onLoginFaild();
     }
    

    } }</code></pre>

    6、最后的 View

    這里的 View 其實就是實現 IPersonView 接口的 Activity ,它必須有一個 Presenter 的實例才能與 Model 交互:

    源碼有刪減,保留核心方法

    /**

  • @author xiarui 16/09/20
  • @description MVP的簡單例子
  • @remark View 必須持有 Presenter 的實例才能與 Model 交互 */ public class MainActivity extends AppCompatActivity implements IPersonView, View.OnClickListener {

    /===== 數據相關 =====/ private PersonPresenter personPersenter;

    @Override protected void onCreate(Bundle savedInstanceState) {

     super.onCreate(savedInstanceState);
     setContentView(R.layout.activity_main);
    
     initView();     //初始化View
     initData();     //初始化Data
    

    }

    /**

    • 初始化Data */ private void initData() { personPersenter = new PersonPresenter(this); }

      @Override public void onClick(View v) { switch (v.getId()) {

       case R.id.bt_main_register:
           if (checkInputInfo()) {
               personPersenter.registerPerson(inputName, inputPwd);
           }
           break;
       case R.id.bt_main_login:
           if (checkInputInfo()) {
               personPersenter.loginPerson(inputName, inputPwd);
           }
           break;
      

      } }

      /========== IPersonView接口方法 START ==========/

      /**

    • 檢查輸入信息的合法性 *
    • @return true:輸入合法,false:輸入不合法 */ @Override public boolean checkInputInfo() { inputName = nameEText.getText().toString().trim(); inputPwd = pwdEText.getText().toString().trim();

      if (inputName.equals("")) {

       nameEText.setError("用戶名不能為空");
       return false;
      

      } if (inputPwd.equals("")) {

       pwdEText.setError("密碼不能為空");
       return false;
      

      } return true; }

      @Override public void onRegisterSucceed() { showToast("注冊成功"); }

      @Override public void onRegisterFaild() { showToast("用戶已存在"); }

      @Override public void onLoginSucceed() { showToast("登錄成功"); }

      @Override public void onLoginFaild() { showToast("用戶不存在或密碼錯誤"); }

      /========== IPersonView接口方法 END ==========/ }</code></pre>

      當完成這些步驟后,一個簡單的 MVP 示例就完成了。

      Q & A

      這里是一些疑問和解答:

      Q: MVP 模式中 View 層是否就是 Activity ?

      A:其實嚴格意義上來說,這么說是不對的。雖然本例中確實是 Activity ,但是在真正的項目中,需要考慮 ActivityFragment 的情況,甚至還要考慮一些特定的 View 或者 ViewGroup

      注:后面我就用 Activity 統一指代 View 了。

      Q:從例子上看,幾乎每一個 Activity 都對應著 一個 Presenter ,還需要其他的接口,那如果 Activity 很多怎么辦?

      A:其實這個問題一直是 MVP 飽受詬病的地方,雖然 MVP 結構很清晰,但確實要增加很多很多的類,所以需要盡量讓接口能適用于多種 View ,但如果實在忍受不了,建議不用 MVP

      Q:使用 MVP 后感覺項目更加臃腫和復雜了怎么辦?

      A:從來都沒有人說過 MVP 能使得項目簡單,只是它會讓項目結構更加清晰更加易于擴展而已。就像 RxJava 一樣,代碼量還是那么多,但是流程更加清晰了,這就是能讓開發者擁護的原因。

      Q:為什么案例中 IPersonView 這個接口將注冊登錄成功與否分開成獨立方法?

      A:這里確實可以不分開,只要將注冊/登錄的結果作為參數即可,但是這樣的話,我們仍然需要在 Activity 中根據結果參數來決定顯示的 Toast 內容。

      也就是說 View 仍然需要處理一些來自 Model 的邏輯,這樣不是太符合 MVP 的意義。所以將判斷邏輯放在 Presenter 中處理, View 層只管展示就行了。

      包括鴻洋大神的那篇文章中,有一個 View 的方法直接傳遞了涉及 Model 層的類,顯然違背了 MVP 的定義,我覺得不是太好(批判了大神,果斷逃……)。

      Q: Presenter 如果進行耗時操作,但此時對應的 Activity 被殺死,會報空指針么?

      A:其實在這種情況下,已經存在內存泄漏的情況了。但有意思的是,并不會報空指針,具體原因暫時還不是特別清楚,但好友 xiasuhuei321 提醒我說,可能回收的時候并沒有完全回收,因為系統會認為還存在相關的引用,所以不會空指針。

      Q:那該如何避免內存泄漏這種情況呢?

      A:這個問題我看的時候覺得很簡單,后來發現這是很有趣的問題。具體方法有很多,也有很多的開源庫專門處理這樣的問題。其實解決辦法歸納起來就是一個 如何讓 Presenter 的生命周期跟 Activity 的生命周期保持一致 。

      我看了很多方法,只覺得通過 Loader 的方法來解決是最簡單也最有效的方法。但是我還沒有徹底學完,暫時不班門弄斧,有興趣可以直接點擊下面的鏈接進行學習:

      總結

      到此,關于 MVP 的簡單入門級知識大概就說完了,雖然網上教程很多很多,但還是用自己的話去講清楚比較舒服。當然了, MVP 可遠遠不止這些,其他的東西學到之后再提吧。

      不過就像開頭說的那樣,這東西就是一個思想,沒必要死板硬套,再者說了谷歌不是又推出了 MVVM 了么。說到 MVVM 又頭疼,感覺總有學不完的東西,雖然總比別人慢一步,但是沒辦法,學技術得冷靜。

      當別人大張旗鼓的時候,更要謀自己的路,證自己的道。

      參考資料

      下面兩篇是我的入門教程,寫的不錯:

      在Android開發中使用MVP模式 – 泡網

      淺談 MVP in Android – Hongyang

      下面這個確實對得起標題,真的很詳細,主要是一些資源綜合,有上下兩篇,這里只貼上篇,都很有價值:

      Android MVP 詳解(上)- diygreen

      下面這個是我朋友寫的,也很詳細而清晰,例子也很具有代表性:

      Android之MVP初嘗試 – xiasuhuei321

      哦對了,這是 MD 風格控件的開源庫,扔物線大神的:

      MaterialEditText – rengwuxian

       

       

      來自:http://www.iamxiarui.com/2016/09/20/android:聊聊我所理解的mvp/

       

 本文由用戶 boblisweer 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!