一步步帶你精通MVP

從MVP開發模式至今,其實已經過了好久;很多開發者也已經輕車熟路的運用到了項目中,本來猶豫要不要寫這篇文章,后來發現還是有人在問MVP怎么用,于是有了這篇文章。

MVP模式本身其實很簡單,一些開發者難以理解,或許是因為要么直接一個Demo下來了,要么一些資料寫的思路不是那么清晰,那么本篇文章以幾個問題作為引導,先幫助不理解的開發者們了解一下MVP的理念是什么,關于架構理念的理解,也可以參考之前的
移動架構這么多,如何一次搞定所有

1. 為什么使用MVP模式?

答:這個問題其實問的是MVP的使用場景。每個項目的規模不同,業務不同,適用于不同的開發模式與架構,不要為了使用架構而去引入架構,要先問一下開發者自己,當前項目需要架構么?當前項目適合什么樣的架構?架構千千萬,但不是所有的架構都具有普適性。這個問題的目的其實是問MVP模式能解決什么問題?那么我們來分析一下。

為什么引入架構呢?如果一個項目,每個類3-500行代碼就解決了,引入架構也就是玩玩而已。這時候重度引入架構反而影響了運行效率,得不償失。 引入架構的項目,必是到了一定的規模,也就是出現了一定程度的耦合與冗余,也一定意義上違反了面向對象的單一職責原則。

那么MVP解決的問題就很明顯了, 那就是冗余、混亂、耦合重。此時拋開MVP不講,如果要我們自己想辦法去解決,如何來解決呢?

分而治之, 我們可能會想到,根據單一職責原則,Activity或Fragment或其他組件冗余了,那么必然要根據不同的功能模塊,來劃分出來不同的職責模塊,這樣也就遵循了單一職責的原則。站在前人的智慧上,或許很多人就想到了M(Model)V(View)C(Controller)。我們可以借鑒這一開發模式,來達到我們的目的,暫時將一個頁面劃分為

UI模塊,也即View層
Model模塊,也即數據請求模塊
Logic模塊, 司邏輯處理

這樣劃分首先職責分工就明確了,解決了混亂,冗余的問題。其實,一個項目從分包,到分類,最后拆分方法實現,都是遵從單一職責;一個職責劃分越具有原子性, 它的重用性就越好,當然這也要根據實際業務而定。比如以下代碼

public class LoginActivity extends AppCompatActivity {

    EditText inputUserName;
    EditText inputPassword;
    Button btnLogin;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        btnLogin.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                final String userName = inputUserName.getText().toString();
                final String password = inputPassword.getText().toString();

                boolean isEmptyUserName = userName == null || userName.length() == 0;
                boolean isEmptyPassword = userName == null || userName.length() == 0;

                boolean isUserNameValid =Pattern.compile("^[A-Za-z0-9]{3,20}+$").matcher(userName   ).matches();
                boolean isPasswordValid = Pattern.compile("^[A-Za-z0-9]{3,20}+$").matcher(password ).matches();

                if (isEmptyPassword || isEmptyPassword) {
                    Toast.makeText(LoginActivity.this, "請輸入帳號密碼", Toast.LENGTH_SHORT).show();
                } else {
                    if (isUserNameValid && isPasswordValid) {
                        new Thread(new Runnable() {
                            @Override
                            public void run() {
                                // ...登錄請求
                                boolean loginResult = false;

                                if (loginResult) {
                                    Toast.makeText(LoginActivity.this, "登錄成功", Toast.LENGTH_SHORT).show();
                                } else {
                                    Toast.makeText(LoginActivity.this, "登錄失敗", Toast.LENGTH_SHORT).show();
                                }
                            }
                        }).start();
                    } else {
                        Toast.makeText(LoginActivity.this, "帳號密碼格式錯誤", Toast.LENGTH_SHORT).show();
                    }
                }
            }
        });
    }
}

一個簡單的登錄, 包括點擊事件, 取登錄信息, 判斷是否空, 校驗是否正確, 請求登錄, 返回處理。這樣的代碼結構混亂, 可讀性差; 代碼冗余,可重用性差; 不同功能的代碼糅合在一起, 耦合性高。這只是很簡單的一個小功能。

上面說到, 面向對象的單一職責原則, 一個模塊劃分越具有原子性,也即劃分越細,那么重用性就越高。如果我改成這樣

public class LoginActivity extends AppCompatActivity {

    EditText inputUserName;
    EditText inputPassword;
    Button btnLogin;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        btnLogin.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                final String userName = getEditorText(inputUserName);
                final String password = getEditorText(inputPassword);

                if (isEmpty(userName) || isEmpty(password)) {
                    showTips("請輸入帳號密碼");
                } else {
                    if (isValid(userName) && isValid(password)) {
                        // 登錄
                        doLogin(userName, password);
                    } else {
                        showTips("帳號密碼格式錯誤");
                    }
                }
            }
        });
    }

    private boolean isValid(String s) {
        return Pattern.compile("^[A-Za-z0-9]{3,20}+$").matcher(s).matches();
    }

    private boolean isEmpty(String s) {
        return s == null || s.length() == 0;
    }

    private String getEditorText(EditText et) {
        return et.getText().toString();
    }

    private void showTips(String tips) {
        Toast.makeText(LoginActivity.this, tips, Toast.LENGTH_SHORT).show();
    }

    private void doLogin(String username, String password) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                // ...登錄請求
                boolean loginResult = false;
                // 更新UI
                notifyLoginResult(loginResult);
            }
        }).start();
    }

    private void notifyLoginResult(boolean loginResult) {
        if (loginResult) {
            showTips("登錄成功");
        } else {
            showTips("登錄失敗");
        }
    }

}

將源碼方法進行拆分后, isEmpty, isValid, showTips..等,產生的結果有亮點:
1) 方法拆分后,可重用性提高了
2) 相比而言,瀏覽一遍,我能基本清楚onClick里做了什么,也就是架構清晰了

這就是單一職責原則的作用,提高可重用性, 減少代碼冗余,開始露出清晰的思維脈絡。

有人就問了,這只是很簡單的封裝啊,有什么意義呢? 那我要反問了,什么是構架?構架的目的是什么?架構無非是從宏觀到細微處得代碼設計與調節,無論大的方向,還是小的細節,都需要慎重設計。構架的目的自然是為了更好的重用,擴展,解耦,以達到更好的代碼健壯性, 擴展性, 提高開發效率。

以上說明了單一職責的意義,以及帶來的附加的益處。那么代碼經過初步重構以后, 雖然更清晰了,消除了冗余,但是耦合的問題依舊。那怎么解決耦合問題呢?我們來看下半場


一步步讓你精通MVP

Step01:MVP實現第一步, 將頁面拆分為M/V/P三個模塊

MVP的概念太簡單, 就是將一個頁面劃分為三部分: M(Model-數據請求/查詢), V(View-UI更新), P(Presenter)。

以上問題從方法的層面解決了單一職責(方法的原子性拆分), 那么整個頁面還是不同的功能糅合在一起,怎么解決呢?就是上面說的劃分為MVP三個部分,每一部分只負責單一的功能,比如

View 只負責UI
Model 只負責數據查詢
Presenter 只負責邏輯處理

MVP最難的難點之一: 如何正確劃分各模塊

Model很簡單, 數據加載的界限很明確,很簡單就劃分出來了, 比如數據庫操作, 比如文件查詢, 比如網絡請求, 可以連帶著異步操作一起拿出來,劃分為單獨的Model層。

View層與Presenter層交互性很頻繁,很多人不清楚這一塊代碼算是View,還是Presenter呢?
首先, 單純的邏輯實現必然是Presenter處理的;單純的View初始化也必然是View處理的,如findView這些。
像登錄模塊,View與邏輯交錯在一起,怎么區分呢 ? 我來給你分

首先Login功能大抵分為以下子功能:

取值, EditText帳號與密碼(明確的View層,不涉及邏輯操作)
判空與校驗 (Presenter但涉及View, 因為使用帳號與密碼,通過傳參的形式)
登錄請求 (名副其實的Model, 處理明顯在Presenter層)
更新UI (View層)

其實以上劃分界限相對比較清晰,項目中難免遇到一些不好界限的,教你一招

難以劃分的必然包含View也包含邏輯處理。那么第一步,原子性拆分,將View與邏輯處理單獨拆分成不同的方法。View 的部分在View層, 處理的部分在Presenter層
有一些Toast, Dialog等的劃分,根據Context作區分。 可以使用Application Context實現的,可以作為Presenter層; 必須使用Activity Context的,作為View層

那么明確了M V P的拆分,看一下拆分結果

View 部分

public class LoginActivity extends AppCompatActivity {

    EditText inputUserName;
    EditText inputPassword;
    Button btnLogin;

    LoginPresenter presenter;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        presenter = new LoginPresenter(this);

        btnLogin.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                presenter.execureLogin(getEditorText(inputUserName), getEditorText(inputPassword));
            }
        });
    }

    private String getEditorText(EditText et) {
        return et.getText().toString();
    }

    public void showTips(String tips) {
        Toast.makeText(LoginActivity.this, tips, Toast.LENGTH_SHORT).show();
    }

    public void notifyLoginResult(boolean loginResult) {
        if (loginResult) {
            showTips("登錄成功");
        } else {
            showTips("登錄失敗");
        }
    }

}

Model部分

public class LoginModel {
    private Handler handler;

    public LoginModel() {
        handler = new Handler();
    }

    public interface OnLoginCallback {
        void onResponse(boolean success);
    }

    public void login(String username, String password, final OnLoginCallback callback) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                // ...請求接口
                boolean result = true; // 假設這是接口返回的結果
                callback.onResponse(result);
            }
        }).start();
    }
}

Presenter部分

public class LoginPresenter {

    private LoginModel model;
    private LoginActivity activity;
    private String verifyMsg;

    public LoginPresenter(LoginActivity activity) {
        this.activity = activity;
        model = new LoginModel();
    }

    public void execureLogin(String username, String password) {
        boolean verifyBefore = verifyBeforeLogin(username, password);
        if (verifyBefore) {
            // 校驗通過,請求登錄
            model.login(username, password, new LoginModel.OnLoginCallback() {
                @Override
                public void onResponse(boolean success) {
                    // 登錄結果
                    activity.notifyLoginResult(success);
                }
            });
        } else {
            // 校驗失敗,提示
            activity.showTips(verifyMsg);
        }
    }

    private boolean verifyBeforeLogin(String username, String password) {
        boolean isEmpty = isEmpty(username) || isEmpty(password);
        boolean isValid = isValid(username) && isValid(password);
        if (isEmpty) {
            verifyMsg = "請輸入帳號或密碼";
            return false;
        }
        if (isValid) {
            return true;
        }
        verifyMsg = "帳號或密碼錯誤";
        return false;
    }

    private boolean isValid(String s) {
        return Pattern.compile("^[A-Za-z0-9]{3,20}+$").matcher(s).matches();
    }

    private boolean isEmpty(String s) {
        return s == null || s.length() == 0;
    }
}

通過以上代碼可以看出,Toast提示, 更新登錄狀態等, 都拆分在View層; 校驗與登錄則拆分在Presenter層;網絡請求則拆分到了Model層。這樣每一層都只處理本層的業務,從大的方向上進行了單一職責拆分,從而整體符合單一職責原則。


根據MVP將頁面拆分為了3層,單一職責的原則我們已經完全符合了。但是仔細看,忽然發現相互之間還存在依賴,解耦效果并不是那么理想。那我們要思考了,是什么原因導致耦合尚在? 那就是對象持有,看看我們的項目

Presenter持有View(Activity)對象,同時持有Model對象
View持有Presenter對象

所以要在持有對象上下功夫了, MVP是怎么解決對象持有問題的?

面向接口編程

Step02: MVP實現第2步, 使用接口通信,進一步解耦

對于面向對象設計來講, 利用接口達到解耦目的已經是人盡皆知的了。 這次改動很小,把對象持有改為接口持有即可。

View持有Presenter對象改為持有View接口
Presenter持有View對象改為持有View接口

既然持有接口,肯定要在View與Presenter分別實現供外部調用的接口。View供Presenter調用的方法有notifyLoginResult和showTips; Presenter供View調用的方法有executeLogin。 那么先來實現接口如何?看代碼

Presenter接口

public interface IPresenter {
    /**
     * 執行登錄
     *
     * @param username
     * @param password
     */
    void executeLogin(String username, String password);
}

View接口

public interface IView {
    /**
     * 更新登錄結果
     *
     * @param loginResult
     */
    void notifyLoginResult(boolean loginResult);

    /**
     * Toast提示
     *
     * @param tips
     */
    void showTips(String tips);
}

接口的作用是對外部提供一種供外部調用的規范。因此這里我們把外部需要調用的方法抽象出來,加入到接口中。接口有了,且接口代表的是View或Presenter的實現,所以分別實現它們??创a

Presenter實現接口

public class LoginPresenter implements IPresenter {

    private LoginModel model;
    private LoginActivity activity;
    private String verifyMsg;

    public LoginPresenter(LoginActivity activity) {
        this.activity = activity;
        model = new LoginModel();
    }

    @Override
    public void executeLogin(String username, String password) {
        boolean verifyBefore = verifyBeforeLogin(username, password);
        if (verifyBefore) {
            // 校驗通過,請求登錄
            model.login(username, password, new LoginModel.OnLoginCallback() {
                @Override
                public void onResponse(boolean success) {
                    // 登錄結果
                    activity.notifyLoginResult(success);
                }
            });
        } else {
            // 校驗失敗,提示
            activity.showTips(verifyMsg);
        }
    }

    private boolean verifyBeforeLogin(String username, String password) {
        boolean isEmpty = isEmpty(username) || isEmpty(password);
        boolean isValid = isValid(username) && isValid(password);
        if (isEmpty) {
            verifyMsg = "請輸入帳號或密碼";
            return false;
        }
        if (isValid) {
            return true;
        }
        verifyMsg = "帳號或密碼錯誤";
        return false;
    }

    private boolean isValid(String s) {
        return Pattern.compile("^[A-Za-z0-9]{3,20}+$").matcher(s).matches();
    }

    private boolean isEmpty(String s) {
        return s == null || s.length() == 0;
    }
}

View實現接口

public class LoginActivity extends AppCompatActivity implements IView{

    EditText inputUserName;
    EditText inputPassword;
    Button btnLogin;

    LoginPresenter presenter;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        presenter = new LoginPresenter(this);

        btnLogin.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                presenter.executeLogin(getEditorText(inputUserName), getEditorText(inputPassword));
            }
        });
    }

    private String getEditorText(EditText et) {
        return et.getText().toString();
    }

    @Override
    public void showTips(String tips) {
        Toast.makeText(LoginActivity.this, tips, Toast.LENGTH_SHORT).show();
    }

    @Override
    public void notifyLoginResult(boolean loginResult) {
        if (loginResult) {
            showTips("登錄成功");
        } else {
            showTips("登錄失敗");
        }
    }

}

這一步很簡單,在接口中提供對外部調用的方法,然后分別在View和Presenter中實現它們。接口與實現都有了,還記得我們的目的是什么嗎?是把持有的對象替換為接口,擼起來,看代碼

// 這是View持有的接口,在onCreate中初始化的對象由原來的LoginPresenter改為了IPresenter。
public class LoginActivity extends AppCompatActivity implements IView{

    EditText inputUserName;
    EditText inputPassword;
    Button btnLogin;

    IPresenter presenter;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        presenter = new LoginPresenter(this);
        ...
}

// 這是Presenter持有的接口,在構造中由原來的LoginActivity改為了IView

public class LoginPresenter implements IPresenter {

    private LoginModel model;
    private String verifyMsg;

    private IView activity;

    public LoginPresenter(IView activity) {
        this.activity = activity;
        model = new LoginModel();
    }

    ...

通過以上重構,我們就實現了傳入接口達到解耦的目的。怎么樣,如果一步步的來,其實一點都不難吧。那么我們來總結一下MVP模式吧

MVP遵從的面向對象原則

1) 單一職責

每個模塊只負責該模塊的本職工作,不越界。 如View負責UI初始化與更新, Model負責數據查詢與異步, 至于邏輯判斷,業務實現,放心的扔給Presenter中就好了。

2) 面向接口通信

對象的持有是造成耦合的本質原因之一,因此要達到解耦的目的,替換為接口持有最是合適不過。

MVP模式的難點

.代碼塊歸屬模塊的劃分

當一塊代碼,你不知道如何劃分模塊,99%的原因是不具備原子性。整篇文字其實我都在強調原子性,也在強調原子性帶來的好處。包括子系統的原子性(插件化), 子模塊的原子性(組件化), 層的原子性(MVX)以及方法的原子性(最小域方法拆分)。

因此要正確的找到分離點, 劃分每一個CodeBlock從屬于什么層, 那么先進行原子性拆分吧。

MVP流程總結

1) 以層為關注點進行分別設計

說白了就是先定義3個層次:View Model Presenter, 然后把正確的代碼分別規劃到對應的層中。

2) 設計通信接口

核心是明白接口的意義,對外部層提供統一調用的規范。目標是外部層,如Presenter要調用View層,那么這就是View層的接口要考慮的,反之亦然。注意,不是所有View中的method都加到IView的接口中

3) BaseInterface 與 Contract的概念

MVP引入了BaseInterface 與Contract的概念。如果單純的mvp,可能很多人都理解,但是加上這兩個概念,加深了理解難度。

base-interface 就是我們常用的base的概念,目的就是規范統一的操作。比如顯示一個Toast, 判斷網絡是否連接,跳轉動畫等,我們都放在BaseActivity中,因為所有的Activity都需要這些。接口的繼承也是這個目的。如
登錄功能

1) 我們需要一個Presenter,于是有了LoginPresenter
2) 我們需要一個LoginPresenter的接口,為View層提供調用,于是有了ILoginPresenter
3) 無論登錄,還是注冊,還有其他功能,所有的Presenter都需要一個功能start, 于是有了IPresenter

  1. IPresenter提供了一個所有Presenter接口共有的操作,就是start,也即初始化的加載
Contract的概念

這個概念的引入只是為了統一管理一個頁面的View和Presenter接口。每個頁面對應一個View(Activity或Fragment), 一個IView(View接口), 一個Presenter, 一個IPresenter(Presenter接口),一個Contract(一個包含View接口和Presenter接口的接口)。如

public interface LoginContract {

    interface View {
        void notifyLoginResult();
    }

    interface Presenter {
        void login(String username, String password);
    }

}

說白了就是一個倉庫,又放水果,又放蔬菜;而不是沒有倉庫時,蔬菜和水果扔的滿地都是,這樣既不好管理,也不好看。類似你會創建chat(聊天), circle(朋友圈),mine(我的),contacts(通訊錄)等包,而不是直接在com.tecent.wx下面放所有的類一樣。


下面帶你從頭到尾封裝一個完整的MVP框架。

1) 首先來思考,我們最先定義的應該是什么? 當然是公共接口。

View的公共接口(MVP-Samples中的IView)沒有公共的操作,我們定義一個空的接口,用于統一規范。

public interface IView {
}

Presenter的公共接口(MVP-Samples中的IPresenter)也沒有公共的操作,在mvp提供的samples中是帶了一個start的,但是這里不需要。為什么呢?因為我們還要來一個BasePresenter。所以我們還是定義一個空的接口,用于統一規范。

public interface IPresenter {
}

以上兩個接口,是用于給View與Presenter的接口繼承的,注意,不是View或Presenter本身繼承。因為它定義的是接口的規范, 而接口才是定義的類的規范。

2) 有了接口規范,我們就需要用接口繼承該規范,因為接口是隨著業務產生的,因此等有了接口再繼承。

3) 開發模式本身是為了業務而生,因此我們生成一個業務,這里以登錄為例。先來分析下業務:

  1. EditText取得輸入的UserName與Password

  2. 校驗(包括判斷是否是空的, 是否符合輸入規范比如不允許輸入特殊字符)

  3. 校驗通過, 執行登錄請求; 不通過,直接提示錯誤

  4. 登錄請求

  5. 根據登錄結果,提示登錄成功或失敗

伴隨著登錄結果更新UI

以上功能很容易就分好層了。

View
提示錯誤信息 / 提示登錄結果
獲取EditText的UserName和Password

Presenter
校驗
執行登錄操作

Model
登錄請求,返回結果

根據功能定義接口了,分別定義一個View的接口和Presenter的接口。還記得上面說的Contract與base-interface嗎? 是的,定義的接口要繼承IView與IPresenter, 而且由Contract統一管理

public interface LoginContract {

    interface View extends IView {

        // View中的2個功能:
        // 1) 取得登錄需要的username, password # 不需要對Presenter層提供調用
        // 2) 提示錯誤信息, 提示登錄結果 # 需要Presenter層調用,因為校驗和登錄都是在Presenter層的
        // 因此2)是View層提供的對外方法,需要在接口中定義

        /**
         * 提示一個Toast
         *
         * @param msg
         */
        void showToast(String msg);

    }

    interface Presenter extends IPresenter {

        // Presenter中的2個功能:
        // 1) 校驗 # 看你怎么寫,既可以在View層中調用校驗方法,也可以在Presenter層中,這里定義為直接在Presenter中校驗,徹底和View解耦
        // 2) 登錄 # 先執行校驗,再執行登錄,需要在View層點擊登錄時調用
        // 因此2)是Presenter對外層提供的方法,需要在接口中定義

        /**
         * 登錄操作
         *
         * @param username
         * @param password
         */
        void login(String username, String password);

    }
}

以上Contract(稱之為功能倉庫)分別定義了View與Presenter接口,并添加了接口的定義過程分析。

4) 接口定義完成了,下一步是什么呢? 肯定是實現接口,加入功能吧。定義Presenter與View分別實現接口,加入對應功能。

public class LoginActivity extends AppCompatActivity implements LoginContract.View {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

    }

    @Override
    public void showToast(String msg) {
        Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
    }
}

**********************************************************************************************************
public class LoginPresenter implements LoginContract.Presenter {

    @Override
    public void login(String username, String password) {
        // 校驗直接放在登錄流程
        boolean isVerifySuccessully = verifyLoginInfo();
        if (isVerifySuccessully) {
            // 請求登錄
            LoginModel.requestLogin(username, password, new LoginModel.RequestCallback() {
                @Override
                public void onResponse(boolean result) {
                    if (result) {
                        // 提示登錄成功
                    } else {
                       // 提示登錄失敗
                    }
                }
            });
        } else {
            // 校驗失敗,提示錯誤
        }
    }

    private boolean verifyLoginInfo() {
        // 這里校驗登錄信息
        // 校驗帳號,密碼是否為空
        // 校驗帳號,密碼是否符合要求
        return true;
    }
}


以上分別實現功能接口,生成了LoginActivity和LoginPresenter。 有些操作和架構無關,比如校驗和登錄請求,都知道怎么做,就不寫了。這里只強調框架。

因為還沒有持有對象,現在還不能相互調用。那么實現了功能,下一步該做什么呢? 那就是層通信了,將兩個層次View和Presenter關聯起來形成完整的功能。

public class LoginActivity extends AppCompatActivity implements LoginContract.View {

    LoginContract.Presenter mPresenter;

    Button loginBtn;
    EditText etUser, etPwd;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mPresenter = new LoginPresenter(this);

        loginBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mPresenter.login(etUser.getText().toString(), etPwd.getText().toString());
            }
        });
    }

    @Override
    public void showToast(String msg) {
        Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
    }

*********************************************************************************************************
public class LoginPresenter implements LoginContract.Presenter {

    LoginContract.View mView;

    public LoginPresenter(LoginContract.View mView) {
        this.mView = mView;
    }

    @Override
    public void login(String username, String password) {
        // 校驗直接放在登錄流程
        boolean isVerifySuccessully = verifyLoginInfo();
        if (isVerifySuccessully) {
            // 請求登錄
            LoginModel.requestLogin(username, password, new LoginModel.RequestCallback() {
                @Override
                public void onResponse(boolean result) {
                    if (result) {
                        // 提示登錄成功
                        mView.showToast("登錄成功");
                    } else {
                        // 提示登錄失敗
                        mView.showToast("登錄失敗");
                    }
                }
            });
        } else {
            // 校驗失敗,提示錯誤
            mView.showToast("無效的帳號或密碼");
        }
    }

    private boolean verifyLoginInfo() {
        // 這里校驗登錄信息
        // 校驗帳號,密碼是否為空
        // 校驗帳號,密碼是否符合要求
        return true;
    }
}

在LoginActivity的onCreate中,我們綁定了Presenter初始化,聲明的是一個接口。在LoginPresenter的構造中,我們同時傳入的View的接口。此時,View層與Presenter層相互引用的是對方的接口。在LoginActivity中模擬了登錄操作,此時View和Presenter層的功能已經完整的關聯在一起了。

注意:LoginActivity在相對的生命周期中需要銷毀Presenter引用,由于后面會封裝,這里沒加。

走到這一步基本就是一個完整的MVP開發模式了,從劃分層次到接口通信,其實還是挺簡單的,不是么?下面繼續來優化這個框架,我們考慮以下幾個問題:

  1. 每個Activity或者Fragment都要初始化或管理Presenter,累不累?
  2. 同樣的,每個Presenter都要管理View,累不累?

那么,現在來繼續優化一下MVP框架的使用。優化之前,我們先來考慮:

  1. Presenter基類抽取

公共元素有哪些 ?

Presenter公共元素,其實主要有兩個: Context, View接口。注意:Presenter不要傳入Activity的Context;如果需要用到Activity的Context, 那么Presenter層就不單純了。那么只能是Application的Context。

我們獲取Application Context的方式有兩種,AppContext(你的Application)的靜態獲取 和 Activity的getApplicationContext。這里使用傳入的Application Context吧!

很多網上View的獲取是定義一個AttachView的方法, 這里使用在構造中直接傳入。

**
 * Created by archer.qi on 2017/2/6.
 */
public abstract class BasePresenter<AttachView extends IView> {
    private Context mContext;
    private AttachView mView;

    public BasePresenter(Context context, AttachView view) {
        if (context == null) {
            throw new NullPointerException("context == null");
        }
        mContext = context.getApplicationContext();
        mView = view;
    }

    /**
     * 獲取關聯的View
     *
     * @return
     */
    public AttachView getAttachedView() {
        if (mView == null) {
            throw new NullPointerException("AttachView is null");
        }
        return mView;
    }

    /**
     * 獲取關聯的Context
     *
     * @return
     */
    public Context getContext() {
        return mContext;
    }

    /**
     * 清空Presenter
     */
    public void clearPresenter() {
        mContext = null;
        mView = null;
    }

    /**
     * View是否關聯
     *
     * @return
     */
    public boolean isViewAttached() {
        return mView != null;
    }

    /**
     * 網絡是否連接
     *
     * @return
     */
    public boolean isNetworkConnected() {
        if (mContext == null) {
            throw new NullPointerException("mContext is null");
        }
        return NetworkHelper.isNetworkConnected(mContext);
    }

    public abstract void start();

    public abstract void destroy();
}

以上是我們抽取的Presenter基類。實現了:

  1. 初始化時綁定View接口,并在clear時清除接口
  2. 自動獲取ApplicationContext(還是建議不這樣,直接傳Application的Context)
  3. View狀態判定
  4. 網絡連接判斷(因為Presenter中執行網絡請求比較頻繁,你可以根據業務自定義多個方法)
  5. satrt初始化方法與destroy銷毀方法(結合后面的MVPCompatActivity自動銷毀)

注意:在使用View時,請先判斷View狀態;否則View異常銷毀時會報NullPoiterException。
如果有線程或者Handler一定要在destroy中銷毀,避免造成內存泄漏。


繼續看View的優化,包含Activity, Fragment, Layout與Adapter

for Activity

/**
 * MVP - Activity基類
 * Created by archer.qi on 2017/1/24.
 */
public abstract class MVPCompatActivity<T extends BasePresenter> extends RootActivity {
    protected T mPresenter;

    @Override
    protected void onStart() {
        super.onStart();
        if (mPresenter == null) {
            mPresenter = createPresenter();
        }
        mPresenter.start();
    }

    @Override
    protected void onStop() {
        super.onStop();
        mPresenter.clearPresenter();
        mPresenter = null;
    }

    @Override
    public void onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) {
        super.onSaveInstanceState(outState, outPersistentState);
        mPresenter.clearPresenter();
        mPresenter = null;
    }

    /**
     * 創建一個Presenter
     *
     * @return
     */
    protected abstract T createPresenter();

}

for Fragment

/**
 * Created by archer.qi on 2017/3/1.
 */
public abstract class MVPCompatFragment<T extends BasePresenter> extends RootFragment {
    protected T mPresenter;

    @Override
    public void onStart() {
        super.onStart();
        if (mPresenter == null) {
            mPresenter = createPresenter();
        }
        mPresenter.start();
    }

    @Override
    public void onStop() {
        super.onStop();
        if (mPresenter != null) {
            mPresenter.clearPresenter();
            mPresenter = null;
        }
    }

    protected abstract T createPresenter();

}

for Layout

/**
 * @author qichunjie 2018/1/18
 */

public abstract class MVPCompatLayout<T extends BasePresenter> extends RootLayout {

    protected T mPresenter;

    public MVPCompatLayout(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        mPresenter = createPresenter();
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if (mPresenter != null) {
            mPresenter.clearPresenter();
            mPresenter = null;
        }
    }

    protected abstract T createPresenter();
}

for Adapter

/**
 * @author qichunjie 2018/1/22
 */

public abstract class MVPCompatRecyclerAdapter<T, P extends BasePresenter> extends RootRecyclerAdapter<T> {
    protected P mPresenter;

    public MVPCompatRecyclerAdapter(Context context, List data) {
        super(context, data);
    }

    protected abstract P createPresenter();


    @Override
    public void onViewAttachedToWindow(RecyclerViewHolder holder) {
        super.onViewAttachedToWindow(holder);
        mPresenter = createPresenter();
    }

    @Override
    public void onViewDetachedFromWindow(RecyclerViewHolder holder) {
        super.onViewDetachedFromWindow(holder);
        if (mPresenter != null) {
            mPresenter.clearPresenter();
            mPresenter = null;
        }
    }
}

通過繼承以上View的Base, 可以自由實現初始化以及銷毀。輕松實現MVP。

最后,補充的RootActivity, 作為一個Base的Activity,是根據不同的業務決定里面的內容的,因此這里很少

/**
 * @author archer.qi
 *         Created on 2017/6/27.
 */
public abstract class RootActivity extends AppCompatActivity {
    protected Context mContext;
    protected Context mAppContext;

    private View mContentView;

    private Bundle mBundleObj;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mAppContext = getApplicationContext();
        mContext = this;
        mContentView = getLayoutInflater().inflate(getLayoutRes(), null);
        setContentView(mContentView);
        ButterKnife.bind(this);
        init();
    }

    protected abstract int getLayoutRes();

    protected abstract void init();

    /**
     * findViewById
     *
     * @param resId
     * @param <T>
     * @return
     */
    protected <T extends View> T $(int resId) {
        return (T) findViewById(resId);
    }

    /**
     * Toast
     *
     * @param toast
     */
    protected void showToast(String toast) {
        Toast.makeText(this, toast, Toast.LENGTH_SHORT).show();
    }

    /**
     * get a bundle from reuse.
     *
     * @return
     */
    protected Bundle obtainBundle() {
        if (mBundleObj == null) {
            mBundleObj = new Bundle();
        } else {
            mBundleObj.clear();
        }
        return mBundleObj;
    }


}

通過以上分析, 是不是覺得So Easy!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,661評論 25 708
  • 前言 看了下上篇博客的發表時間到這篇博客,竟然過了11個月,罪過,罪過。這一年時間也是夠折騰的,年初離職跳槽到鵝廠...
    西木柚子閱讀 21,277評論 12 184
  • 作者:李旺成 時間:2016年4月3日 “Android MVP 詳解(下)”已經發布,歡迎大家提建議。 MVP ...
    diygreen閱讀 128,931評論 86 1,321
  • 簡易壓縮算法:將全部由小寫英文字母組成的字符串,將其中連續超過兩個相同字母的部分壓縮為整個連續個數加該字母,其他部...
    hainingwyx閱讀 2,320評論 0 0
  • 其實現在想想我實習的狀態真的蠻好的,跟國賀做完那份調研報告的滿足感,跟露露加班完大半夜在回家路上跳華爾茲,呵呵。半...
    不一樣的大象閱讀 633評論 0 50