認(rèn)識(shí)Mvp

從MVP開發(fā)模式至今,其實(shí)已經(jīng)過了好久;很多開發(fā)者也已經(jīng)輕車熟路的運(yùn)用到了項(xiàng)目中,本來(lái)猶豫要不要寫這篇文章,后來(lái)發(fā)現(xiàn)還是有人在問MVP怎么用,于是有了這篇文章。

MVP模式本身其實(shí)很簡(jiǎn)單,一些開發(fā)者難以理解,或許是因?yàn)橐粗苯右粋€(gè)Demo下來(lái)了,要么一些資料寫的思路不是那么清晰,那么本篇文章以幾個(gè)問題作為引導(dǎo),先幫助不理解的開發(fā)者們了解一下MVP的理念是什么,關(guān)于架構(gòu)理念的理解,也可以參考之前的
移動(dòng)架構(gòu)這么多,如何一次搞定所有

1. 為什么使用MVP模式?

答:這個(gè)問題其實(shí)問的是MVP的使用場(chǎng)景。每個(gè)項(xiàng)目的規(guī)模不同,業(yè)務(wù)不同,適用于不同的開發(fā)模式與架構(gòu),不要為了使用架構(gòu)而去引入架構(gòu),要先問一下開發(fā)者自己,當(dāng)前項(xiàng)目需要架構(gòu)么?當(dāng)前項(xiàng)目適合什么樣的架構(gòu)?架構(gòu)千千萬(wàn),但不是所有的架構(gòu)都具有普適性。這個(gè)問題的目的其實(shí)是問MVP模式能解決什么問題?那么我們來(lái)分析一下。

為什么引入架構(gòu)呢?如果一個(gè)項(xiàng)目,每個(gè)類3-500行代碼就解決了,引入架構(gòu)也就是玩玩而已。這時(shí)候重度引入架構(gòu)反而影響了運(yùn)行效率,得不償失。 引入架構(gòu)的項(xiàng)目,必是到了一定的規(guī)模,也就是出現(xiàn)了一定程度的耦合與冗余,也一定意義上違反了面向?qū)ο蟮膯我宦氊?zé)原則。

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

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

UI模塊,也即View層
Model模塊,也即數(shù)據(jù)請(qǐng)求模塊
Logic模塊, 司邏輯處理

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

 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, "請(qǐng)輸入帳號(hào)密碼", Toast.LENGTH_SHORT).show();
                } else {
                    if (isUserNameValid && isPasswordValid) {
                        new Thread(new Runnable() {
                            @Override
                            public void run() {
                                // ...登錄請(qǐng)求
                                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, "帳號(hào)密碼格式錯(cuò)誤", Toast.LENGTH_SHORT).show();
                    }
                }
            }
        });
    }
}

一個(gè)簡(jiǎn)單的登錄, 包括點(diǎn)擊事件, 取登錄信息, 判斷是否空, 校驗(yàn)是否正確, 請(qǐng)求登錄, 返回處理。這樣的代碼結(jié)構(gòu)混亂, 可讀性差; 代碼冗余,可重用性差; 不同功能的代碼糅合在一起, 耦合性高。這只是很簡(jiǎn)單的一個(gè)小功能。
上面說(shuō)到, 面向?qū)ο蟮膯我宦氊?zé)原則, 一個(gè)模塊劃分越具有原子性,也即劃分越細(xì),那么重用性就越高。如果我改成這樣

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("請(qǐng)輸入帳號(hào)密碼");
                } else {
                    if (isValid(userName) && isValid(password)) {
                        // 登錄
                        doLogin(userName, password);
                    } else {
                        showTips("帳號(hào)密碼格式錯(cuò)誤");
                    }
                }
            }
        });
    }

    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() {
                // ...登錄請(qǐng)求
                boolean loginResult = false;
                // 更新UI
                notifyLoginResult(loginResult);
            }
        }).start();
    }

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

將源碼方法進(jìn)行拆分后, isEmpty, isValid, showTips..等,產(chǎn)生的結(jié)果有亮點(diǎn):
1) 方法拆分后,可重用性提高了
2) 相比而言,瀏覽一遍,我能基本清楚onClick里做了什么,也就是架構(gòu)清晰了
這就是單一職責(zé)原則的作用,提高可重用性, 減少代碼冗余,開始露出清晰的思維脈絡(luò)。
有人就問了,這只是很簡(jiǎn)單的封裝啊,有什么意義呢? 那我要反問了,什么是構(gòu)架?構(gòu)架的目的是什么?架構(gòu)無(wú)非是從宏觀到細(xì)微處得代碼設(shè)計(jì)與調(diào)節(jié),無(wú)論大的方向,還是小的細(xì)節(jié),都需要慎重設(shè)計(jì)。構(gòu)架的目的自然是為了更好的重用,擴(kuò)展,解耦,以達(dá)到更好的代碼健壯性, 擴(kuò)展性, 提高開發(fā)效率。
以上說(shuō)明了單一職責(zé)的意義,以及帶來(lái)的附加的益處。那么代碼經(jīng)過初步重構(gòu)以后, 雖然更清晰了,消除了冗余,但是耦合的問題依舊。那怎么解決耦合問題呢?我們來(lái)看下半場(chǎng)

一步步讓你精通MVP

Step01:MVP實(shí)現(xiàn)第一步, 將頁(yè)面拆分為M/V/P三個(gè)模塊

MVP的概念太簡(jiǎn)單, 就是將一個(gè)頁(yè)面劃分為三部分: M(Model-數(shù)據(jù)請(qǐng)求/查詢), V(View-UI更新), P(Presenter)。
以上問題從方法的層面解決了單一職責(zé)(方法的原子性拆分), 那么整個(gè)頁(yè)面還是不同的功能糅合在一起,怎么解決呢?就是上面說(shuō)的劃分為MVP三個(gè)部分,每一部分只負(fù)責(zé)單一的功能,比如
View 只負(fù)責(zé)UI
Model 只負(fù)責(zé)數(shù)據(jù)查詢
Presenter 只負(fù)責(zé)邏輯處理

MVP最難的難點(diǎn)之一: 如何正確劃分各模塊

Model很簡(jiǎn)單, 數(shù)據(jù)加載的界限很明確,很簡(jiǎn)單就劃分出來(lái)了, 比如數(shù)據(jù)庫(kù)操作, 比如文件查詢, 比如網(wǎng)絡(luò)請(qǐng)求, 可以連帶著異步操作一起拿出來(lái),劃分為單獨(dú)的Model層。

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

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

取值, EditText帳號(hào)與密碼(明確的View層,不涉及邏輯操作)
判空與校驗(yàn) (Presenter但涉及View, 因?yàn)槭褂脦ぬ?hào)與密碼,通過傳參的形式)
登錄請(qǐng)求 (名副其實(shí)的Model, 處理明顯在Presenter層)
更新UI (View層)

其實(shí)以上劃分界限相對(duì)比較清晰,項(xiàng)目中難免遇到一些不好界限的,教你一招

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

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

1.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("登錄失敗");
        }
    }
}

2.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() {
                // ...請(qǐng)求接口
                boolean result = true; // 假設(shè)這是接口返回的結(jié)果
                callback.onResponse(result);
            }
        }).start();
    }
}

3.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) {
            // 校驗(yàn)通過,請(qǐng)求登錄
            model.login(username, password, new LoginModel.OnLoginCallback() {
                @Override
                public void onResponse(boolean success) {
                    // 登錄結(jié)果
                    activity.notifyLoginResult(success);
                }
            });
        } else {
            // 校驗(yàn)失敗,提示
            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 = "請(qǐng)輸入帳號(hào)或密碼";
            return false;
        }
        if (isValid) {
            return true;
        }
        verifyMsg = "帳號(hào)或密碼錯(cuò)誤";
        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提示, 更新登錄狀態(tài)等, 都拆分在View層; 校驗(yàn)與登錄則拆分在Presenter層;網(wǎng)絡(luò)請(qǐng)求則拆分到了Model層。這樣每一層都只處理本層的業(yè)務(wù),從大的方向上進(jìn)行了單一職責(zé)拆分,從而整體符合單一職責(zé)原則。

根據(jù)MVP將頁(yè)面拆分為了3層,單一職責(zé)的原則我們已經(jīng)完全符合了。但是仔細(xì)看,忽然發(fā)現(xiàn)相互之間還存在依賴,解耦效果并不是那么理想。那我們要思考了,是什么原因?qū)е埋詈仙性冢?那就是對(duì)象持有,看看我們的項(xiàng)目

Presenter持有View(Activity)對(duì)象,同時(shí)持有Model對(duì)象
View持有Presenter對(duì)象

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

面向接口編程
Step02: MVP實(shí)現(xiàn)第2步, 使用接口通信,進(jìn)一步解耦

對(duì)于面向?qū)ο笤O(shè)計(jì)來(lái)講, 利用接口達(dá)到解耦目的已經(jīng)是人盡皆知的了。 這次改動(dòng)很小,把對(duì)象持有改為接口持有即可。

View持有Presenter對(duì)象改為持有View接口
Presenter持有View對(duì)象改為持有View接口

既然持有接口,肯定要在View與Presenter分別實(shí)現(xiàn)供外部調(diào)用的接口。View供Presenter調(diào)用的方法有notifyLoginResult和showTips; Presenter供View調(diào)用的方法有executeLogin。 那么先來(lái)實(shí)現(xiàn)接口如何?看代碼

Presenter接口

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

View接口

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

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

接口的作用是對(duì)外部提供一種供外部調(diào)用的規(guī)范。因此這里我們把外部需要調(diào)用的方法抽象出來(lái),加入到接口中。接口有了,且接口代表的是View或Presenter的實(shí)現(xiàn),所以分別實(shí)現(xiàn)它們。看代碼

Presenter實(shí)現(xiàn)接口

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) {
            // 校驗(yàn)通過,請(qǐng)求登錄
            model.login(username, password, new LoginModel.OnLoginCallback() {
                @Override
                public void onResponse(boolean success) {
                    // 登錄結(jié)果
                    activity.notifyLoginResult(success);
                }
            });
        } else {
            // 校驗(yàn)失敗,提示
            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 = "請(qǐng)輸入帳號(hào)或密碼";
            return false;
        }
        if (isValid) {
            return true;
        }
        verifyMsg = "帳號(hào)或密碼錯(cuò)誤";
        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實(shí)現(xiàn)接口

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("登錄失敗");
        }
    }

}

這一步很簡(jiǎn)單,在接口中提供對(duì)外部調(diào)用的方法,然后分別在View和Presenter中實(shí)現(xiàn)它們。接口與實(shí)現(xiàn)都有了,還記得我們的目的是什么嗎?是把持有的對(duì)象替換為接口,擼起來(lái),看代碼

// 這是View持有的接口,在onCreate中初始化的對(duì)象由原來(lái)的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持有的接口,在構(gòu)造中由原來(lái)的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();
    }

    ...

通過以上重構(gòu),我們就實(shí)現(xiàn)了傳入接口達(dá)到解耦的目的。怎么樣,如果一步步的來(lái),其實(shí)一點(diǎn)都不難吧。那么我們來(lái)總結(jié)一下MVP模式吧

MVP遵從的面向?qū)ο笤瓌t

1) 單一職責(zé)

每個(gè)模塊只負(fù)責(zé)該模塊的本職工作,不越界。 如View負(fù)責(zé)UI初始化與更新, Model負(fù)責(zé)數(shù)據(jù)查詢與異步, 至于邏輯判斷,業(yè)務(wù)實(shí)現(xiàn),放心的扔給Presenter中就好了。

2) 面向接口通信

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

MVP模式的難點(diǎn)

.代碼塊歸屬模塊的劃分

當(dāng)一塊代碼,你不知道如何劃分模塊,99%的原因是不具備原子性。整篇文字其實(shí)我都在強(qiáng)調(diào)原子性,也在強(qiáng)調(diào)原子性帶來(lái)的好處。包括子系統(tǒng)的原子性(插件化), 子模塊的原子性(組件化), 層的原子性(MVX)以及方法的原子性(最小域方法拆分)。
因此要正確的找到分離點(diǎn), 劃分每一個(gè)CodeBlock從屬于什么層, 那么先進(jìn)行原子性拆分吧。

MVP流程總結(jié)

1) 以層為關(guān)注點(diǎn)進(jìn)行分別設(shè)計(jì)

說(shuō)白了就是先定義3個(gè)層次:View Model Presenter, 然后把正確的代碼分別規(guī)劃到對(duì)應(yīng)的層中。

2) 設(shè)計(jì)通信接口

核心是明白接口的意義,對(duì)外部層提供統(tǒng)一調(diào)用的規(guī)范。目標(biāo)是外部層,如Presenter要調(diào)用View層,那么這就是View層的接口要考慮的,反之亦然。注意,不是所有View中的method都加到IView的接口中

3) BaseInterface 與 Contract的概念

MVP引入了BaseInterface 與Contract的概念。如果單純的mvp,可能很多人都理解,但是加上這兩個(gè)概念,加深了理解難度。
base-interface 就是我們常用的base的概念,目的就是規(guī)范統(tǒng)一的操作。比如顯示一個(gè)Toast, 判斷網(wǎng)絡(luò)是否連接,跳轉(zhuǎn)動(dòng)畫等,我們都放在BaseActivity中,因?yàn)樗械腁ctivity都需要這些。接口的繼承也是這個(gè)目的。如
登錄功能

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

IPresenter提供了一個(gè)所有Presenter接口共有的操作,就是start,也即初始化的加載

Contract的概念

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

public interface LoginContract {

    interface View {
        void notifyLoginResult();
    }

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

}

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

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

  1. 首先來(lái)思考,我們最先定義的應(yīng)該是什么? 當(dāng)然是公共接口。

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

public interface IView {
}

Presenter的公共接口(MVP-Samples中的IPresenter)也沒有公共的操作,在mvp提供的samples中是帶了一個(gè)start的,但是這里不需要。為什么呢?因?yàn)槲覀冞€要來(lái)一個(gè)BasePresenter。所以我們還是定義一個(gè)空的接口,用于統(tǒng)一規(guī)范。

public interface IPresenter {
}

以上兩個(gè)接口,是用于給View與Presenter的接口繼承的,注意,不是View或Presenter本身繼承。因?yàn)樗x的是接口的規(guī)范, 而接口才是定義的類的規(guī)范。

  1. 有了接口規(guī)范,我們就需要用接口繼承該規(guī)范,因?yàn)榻涌谑请S著業(yè)務(wù)產(chǎn)生的,因此等有了接口再繼承。
  2. 開發(fā)模式本身是為了業(yè)務(wù)而生,因此我們生成一個(gè)業(yè)務(wù),這里以登錄為例。先來(lái)分析下業(yè)務(wù):

EditText取得輸入的UserName與Password

校驗(yàn)(包括判斷是否是空的, 是否符合輸入規(guī)范比如不允許輸入特殊字符)

校驗(yàn)通過, 執(zhí)行登錄請(qǐng)求; 不通過,直接提示錯(cuò)誤

登錄請(qǐng)求

根據(jù)登錄結(jié)果,提示登錄成功或失敗

伴隨著登錄結(jié)果更新UI

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

View
提示錯(cuò)誤信息 / 提示登錄結(jié)果
獲取EditText的UserName和Password

Presenter
校驗(yàn)
執(zhí)行登錄操作

Model
登錄請(qǐng)求,返回結(jié)果

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

public interface LoginContract {

    interface View extends IView {

        // View中的2個(gè)功能:
        // 1) 取得登錄需要的username, password # 不需要對(duì)Presenter層提供調(diào)用
        // 2) 提示錯(cuò)誤信息, 提示登錄結(jié)果 # 需要Presenter層調(diào)用,因?yàn)樾r?yàn)和登錄都是在Presenter層的
        // 因此2)是View層提供的對(duì)外方法,需要在接口中定義

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

    }

    interface Presenter extends IPresenter {

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

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

    }
}

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

4) 接口定義完成了,下一步是什么呢? 肯定是實(shí)現(xiàn)接口,加入功能吧。定義Presenter與View分別實(shí)現(xiàn)接口,加入對(duì)應(yīng)功能。

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) {
        // 校驗(yàn)直接放在登錄流程
        boolean isVerifySuccessully = verifyLoginInfo();
        if (isVerifySuccessully) {
            // 請(qǐng)求登錄
            LoginModel.requestLogin(username, password, new LoginModel.RequestCallback() {
                @Override
                public void onResponse(boolean result) {
                    if (result) {
                        // 提示登錄成功
                    } else {
                       // 提示登錄失敗
                    }
                }
            });
        } else {
            // 校驗(yàn)失敗,提示錯(cuò)誤
        }
    }

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

以上分別實(shí)現(xiàn)功能接口,生成了LoginActivity和LoginPresenter。 有些操作和架構(gòu)無(wú)關(guān),比如校驗(yàn)和登錄請(qǐng)求,都知道怎么做,就不寫了。這里只強(qiáng)調(diào)框架。
因?yàn)檫€沒有持有對(duì)象,現(xiàn)在還不能相互調(diào)用。那么實(shí)現(xiàn)了功能,下一步該做什么呢? 那就是層通信了,將兩個(gè)層次View和Presenter關(guān)聯(lián)起來(lái)形成完整的功能。

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) {
       // 校驗(yàn)直接放在登錄流程
       boolean isVerifySuccessully = verifyLoginInfo();
       if (isVerifySuccessully) {
           // 請(qǐng)求登錄
           LoginModel.requestLogin(username, password, new LoginModel.RequestCallback() {
               @Override
               public void onResponse(boolean result) {
                   if (result) {
                       // 提示登錄成功
                       mView.showToast("登錄成功");
                   } else {
                       // 提示登錄失敗
                       mView.showToast("登錄失敗");
                   }
               }
           });
       } else {
           // 校驗(yàn)失敗,提示錯(cuò)誤
           mView.showToast("無(wú)效的帳號(hào)或密碼");
       }
   }

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

在LoginActivity的onCreate中,我們綁定了Presenter初始化,聲明的是一個(gè)接口。在LoginPresenter的構(gòu)造中,我們同時(shí)傳入的View的接口。此時(shí),View層與Presenter層相互引用的是對(duì)方的接口。在LoginActivity中模擬了登錄操作,此時(shí)View和Presenter層的功能已經(jīng)完整的關(guān)聯(lián)在一起了。
注意:LoginActivity在相對(duì)的生命周期中需要銷毀Presenter引用,由于后面會(huì)封裝,這里沒加。
走到這一步基本就是一個(gè)完整的MVP開發(fā)模式了,從劃分層次到接口通信,其實(shí)還是挺簡(jiǎn)單的,不是么?下面繼續(xù)來(lái)優(yōu)化這個(gè)框架,我們考慮以下幾個(gè)問題:

每個(gè)Activity或者Fragment都要初始化或管理Presenter,累不累?
同樣的,每個(gè)Presenter都要管理View,累不累?

那么,現(xiàn)在來(lái)繼續(xù)優(yōu)化一下MVP框架的使用。優(yōu)化之前,我們先來(lái)考慮:

Presenter基類抽取

公共元素有哪些 ?

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

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

很多網(wǎng)上View的獲取是定義一個(gè)AttachView的方法, 這里使用在構(gòu)造中直接傳入。

**
 * 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;
    }

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

    /**
     * 獲取關(guān)聯(lián)的Context
     *
     * @return
     */
    public Context getContext() {
        return mContext;
    }

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

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

    /**
     * 網(wǎng)絡(luò)是否連接
     *
     * @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基類。實(shí)現(xiàn)了:

1.初始化時(shí)綁定View接口,并在clear時(shí)清除接口
2.自動(dòng)獲取ApplicationContext(還是建議不這樣,直接傳Application的Context)
3.View狀態(tài)判定
4.網(wǎng)絡(luò)連接判斷(因?yàn)镻resenter中執(zhí)行網(wǎng)絡(luò)請(qǐng)求比較頻繁,你可以根據(jù)業(yè)務(wù)自定義多個(gè)方法)
5.satrt初始化方法與destroy銷毀方法(結(jié)合后面的MVPCompatActivity自動(dòng)銷毀)

注意:在使用View時(shí),請(qǐng)先判斷View狀態(tài);否則View異常銷毀時(shí)會(huì)報(bào)NullPoiterException。
如果有線程或者Handler一定要在destroy中銷毀,避免造成內(nèi)存泄漏。

繼續(xù)看View的優(yōu)化,包含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;
    }

    /**
     * 創(chuàng)建一個(gè)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, 可以自由實(shí)現(xiàn)初始化以及銷毀。輕松實(shí)現(xiàn)MVP。

最后,補(bǔ)充的RootActivity, 作為一個(gè)Base的Activity,是根據(jù)不同的業(yè)務(wù)決定里面的內(nèi)容的,因此這里很少

/**
 * @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! so TM EZ

!!!!!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容