Android:聊聊我所理解的MVP

寫在前面

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

以前看不少開源軟件實現都是各類 MVP ,看起來很高大上,不過說實話,很早就瞭解 MVP 了,但一直很抗拒去學習,由於以爲模式或者架構類的東西屬於一種思想,並非固定的寫法,而學習思想以前,必需要學會在引進這種思想以前是如何處理這些問題的。html

也就是說,在學 MVP 以前,我得弄明白爲啥會提出 MVP ,由於在 MVP 提出以前都是用 MVC 去處理的,因此我得學會 MVC ,當我能熟練的使用 MVC 的時候,再去學習 MVP ,這樣能很清楚的明白二者之間的區別或者各自的優缺點,我的以爲這樣學起來仍是比較好的,而不是盲目跟風,包括如今不少博客提到的 React NativeDagger2 等等都是同樣的道理。android

如今我也來簡單聊一聊我本身所理解的 MVP ,不過只能算個入門吧。git

不可少的介紹

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

先上一個經典的圖:bash

MVC和MVP

C 和 P 的區別

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

一、先看 Cide

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

換句話說就是,它是一個 ModelView 之間的橋樑,讓 ModelView 之間再也不牢牢關聯。學習

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

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

可是注意,我只是說能很大程度上解決,並不能完全解決,也就是說小明若是發現了隔壁小王有問題,他仍然能夠選擇直接把情書交給小紅。

二、再看 P

P 就是 Presenter,我翻譯成主持者。跟 C 相似,仍然是負責 ViewModel 之間的溝通。可是它完全讓 ViewModel 不能直接溝通。若是想要溝通,就必須經過這個主持者來主持它們兩個應該幹啥。

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

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

這就完全斷了我跟主席……哦不對,ModelView 之間的聯繫。

三、簡單區別

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

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

CModelView 作到 鬆散耦合,而 P 直接將它們 解耦

MVC 和 MVP 的區別

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

一、先看 MVC

從下圖中咱們能夠看到:

MVC

  • 用戶 Event(事件)會致使 Controller 改變 ModelView 或同時改變二者。
  • 只要 Controller 改變了 Model 的數據或屬性,全部依賴的 View 都會自動更新。
  • 相似的,只要 Controller 改變了 ViewView 會從潛在的 Model 中獲取數據進行更新。

二、再看 MVP

從下圖中咱們又能看到:

MVP

  • Presenter 中同時持有 View 以及 ModelInterface 引用,而 View 持有 Presenter 的實例。
  • 當某個 View 須要展現某些數據時,首先會調用 Presenter 的某個接口,而後 Presenter 會調用 Model 請求數據。
  • Model 數據加載成功後會調用 Presenter 的回調方法通知 Presenter 數據加載完畢,最後 Presenter 再調用 View 層接口展現加載後數據。

三、主要區別

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 的接口對象。

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

再動手

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

一、先建一個 Bean

/**
 * @author xiarui 16/09/20
 * @description Person的Bean類
 */
public class PersonBean {
    private String name ;
    private String pwd;
    //...省略
}
複製代碼

二、再創建 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);
}
複製代碼

三、其次創建 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));
    }
}
複製代碼

四、還須要 View Interface

在這裏我設定了五個方法,其中註冊/登陸成功與否分別建了兩個方法,緣由後文再說:

/**
 * @author xiarui 16/09/20
 * @description IPersonView接口
 */
public interface IPersonView {
    boolean checkInputInfo();  //檢查輸入的合法性
  	void onRegisterSucceed();  //註冊成功 
    void onRegisterFaild();    //註冊失敗 
    void onLoginSucceed();     //登陸成功
    void onLoginFaild();       //登陸失敗
}
複製代碼

五、最重要的 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();
        }
    }
}
複製代碼

六、最後的 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 ==========*/
}
複製代碼

當完成這些步驟後,一個簡單的 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 的方法來解決是最簡單也最有效的方法。可是我尚未完全學完,暫時不班門弄斧,有興趣能夠直接點擊下面的連接進行學習:

經過Loader延長Presenter生命週期

總結

到此,關於 MVP 的簡單入門級知識大概就說完了,雖然網上教程不少不少,但仍是用本身的話去講清楚比較舒服。固然了, MVP 可遠遠不止這些,其餘的東西學到以後再提吧。

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

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

參考資料

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

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

淺談 MVP in Android - Hongyang

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

Android MVP 詳解(上)- diygreen

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

Android之MVP初嘗試 - xiasuhuei321

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

MaterialEditText - rengwuxian

項目源碼

FirstMVPDemo - IamXiaRui


我的博客:www.iamxiarui.com

原文連接:www.iamxiarui.com/?p=890

相關文章
相關標籤/搜索