Android MVP 模式解析與基本實(shí)現(xiàn)方式

文章已同步至 CSDN:http://blog.csdn.net/qq_24867873/article/details/79459856

前言

記得自己接手的第二個(gè)項(xiàng)目采用的是 MVP 模式進(jìn)行開發(fā)的,當(dāng)時(shí)架構(gòu)已經(jīng)設(shè)計(jì)好,我看了幾篇關(guān)于 MVP 的文章,對(duì)其有了基本的了解之后,便照貓畫虎進(jìn)行了開發(fā),之后便再也沒接觸過 MVP。

最近空閑的時(shí)候讀了一篇 MVP 相關(guān)的文章,受益匪淺。于是打算寫一篇關(guān)于它的文章,一方面是作為自己的學(xué)習(xí)筆記方便查看,另一反面希望能給沒有接觸過 MVP 模式的新人提供幫助,以便可以快速入門。

什么是 MVC

在講 MVP 之前,我們先來了解一下 MVC。

MVC 結(jié)構(gòu)圖

MVC 模式是經(jīng)典的三層架構(gòu)一種具體的實(shí)現(xiàn)方式,全稱為 Model(模型層) 、View(視圖層)、Controller(控制器)。下面介紹一下它們各自的職責(zé):

  • Model 層:用來定義實(shí)體對(duì)象,處理業(yè)務(wù)邏輯,可以簡單地理解成 Java 中的實(shí)體類。
  • View 層:負(fù)責(zé)處理界面的顯示,在 Android 中對(duì)應(yīng)的就是 xml 文件。
  • Controller 層:對(duì)應(yīng)的是 Activity/Fragment ,當(dāng)加載完成 xml 布局之后,我們需要找到并設(shè)置布局中的各個(gè) View,處理用戶的交互事件,更新 View 等。

下面我們通過一個(gè)簡單的例子來說明這三者是如何交互的。

首先是 View 層,布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical"
              android:padding="16dp">

    <EditText
        android:id="@+id/et_height"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="身高cm"/>

    <EditText
        android:id="@+id/et_weight"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="體重kg"/>

    <Button
        android:id="@+id/btn_cal"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"/>

</LinearLayout>

然后是 Controller 層:

public class MVCActivity extends AppCompatActivity implements View.OnClickListener {

    private EditText mEtHeight;
    private EditText mEtWeight;
    private Button mBtnCal;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Controller 訪問了 View 的組件
        mEtHeight = findViewById(R.id.et_height);
        mEtWeight = findViewById(R.id.et_weight);
        mBtnCal = findViewById(R.id.btn_cal);
        // 這個(gè)點(diǎn)擊事件屬于 View,它是 View 的監(jiān)聽器
        mBtnCal.setOnClickListener(this);

        // Controller 調(diào)用了 Model
        String btnText = User.instance().getBtnText();
        // 然后 Controller 更新了 View 的屬性
        mBtnCal.setText(btnText);
    }

    @Override
    public void onClick(View v) {
        int height = Integer.parseInt(mEtHeight.getText().toString());
        float weight = Float.parseFloat(mEtWeight.getText().toString());
        // Controller 更新了 Model 中的數(shù)據(jù)
        User.instance().setHeight(height);
        User.instance().setWeight(weight);
        // 這里 View 又訪問了 Model 的數(shù)據(jù),并呈現(xiàn)在 UI 上
        String valueBMI = String.valueOf(User.instance().getBMI());
        Toast.makeText(this, "BMI: " + valueBMI, Toast.LENGTH_LONG).show();
    }
}

最后是 Model 層:

public class User {

    private int height;
    private float weight;

    private static User mUser;

    public static User instance(){
        if (mUser == null) {
            synchronized (User.class) {
                if (mUser == null) {
                    mUser = new User();
                }
            }
        }
        return mUser;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public float getWeight() {
        return weight;
    }

    public void setWeight(float weight) {
        this.weight = weight;
    }

    public String getBtnText() {
        // 在這里,我們可以從數(shù)據(jù)庫中查詢數(shù)據(jù)
        // 或者訪問網(wǎng)絡(luò)獲取數(shù)據(jù)
        return "計(jì)算BMI";
    }

    public float getBMI() {
        // 通過已有的屬性計(jì)算出新的屬性,也屬于業(yè)務(wù)邏輯的操作
        return weight / (height * height) * 10000;
    }
}

從上面的代碼中,我們可以看到 View 層的職責(zé)是非常簡單的,向用戶呈現(xiàn) xml 文件中的布局,并且響應(yīng)用戶的觸摸事件。

而 Controller 層的職責(zé)邏輯則復(fù)雜很多。它對(duì)于 View 層,需要將從 Model 中獲取到的數(shù)據(jù)及時(shí)地呈現(xiàn)在 UI 上。而對(duì)于 Model 層,當(dāng) app 的生命周期發(fā)生變化或者接收到某些響應(yīng)時(shí),需要對(duì) Model 的數(shù)據(jù)進(jìn)行 CRUD。在這個(gè)例子中,用戶點(diǎn)擊按鈕的時(shí)候,首先獲取 View 層用戶的輸入,然后更新 Model 層的屬性,最后獲取到 Model 層計(jì)算得出的新數(shù)據(jù)并顯示在 UI 上。

對(duì)于 Model 來說,它不僅僅是個(gè)簡單的實(shí)體類,還應(yīng)該包括數(shù)據(jù)處理與業(yè)務(wù)邏輯的操作,比如說對(duì)數(shù)據(jù)庫的操作、網(wǎng)絡(luò)請(qǐng)求等,但是很多情況下,我們很少把這些操作寫在實(shí)體類中。

demo 運(yùn)行效果如下:

運(yùn)行效果

在 MVC 模式中,Controller 層扮演著重要的角色,它不僅要處理 UI 的顯示與事件的響應(yīng),還要負(fù)責(zé)與 Model 層的通信,同時(shí) Model 層與 View 層也會(huì)通信,三者的耦合度很大。

作為 Android 開發(fā)中默認(rèn)使用的架構(gòu)模式,MVC 易于上手,適合快速開發(fā)一些小型項(xiàng)目。但是隨著業(yè)務(wù)邏輯的復(fù)雜度越來越大,Activity/Fragment 會(huì)越來越臃腫,因?yàn)樗瑫r(shí)承擔(dān)著 Controller 與 View 的角色,這對(duì)于項(xiàng)目后期的更新維護(hù)與測試交接都是非常不方便的,大大提高了生產(chǎn)成本。這么一來,它就違背了 “提高生產(chǎn)力” 的初衷,于是 MVP 模式就應(yīng)運(yùn)而生了。

什么是 MVP

MVP 結(jié)構(gòu)圖

MVP 是 MVC 的一種升級(jí)進(jìn)化,全稱為 Model(模型層)、View(視圖層)、Presenter(主持者)。從結(jié)構(gòu)圖中,我們可以看到它與 MVC 的區(qū)別:Presenter 代替了 Controller,去除了 View 與 Model 的關(guān)聯(lián)與耦合。

  • Model 層:和 MVC 模式中的 Model 層是一樣的,這里不再說了。
  • View 層:視圖層。在 MVP 中,它不僅僅對(duì)應(yīng) xml 布局了,Activity/Fragment 也屬于視圖層。View 層現(xiàn)在不僅作為 UI 的顯示,還負(fù)責(zé)響應(yīng)生命周期的變化。
  • Presenter 層:主持者層,是 Model 層與 View 層進(jìn)行溝通的橋梁,處理業(yè)務(wù)邏輯。它響應(yīng) View 層的請(qǐng)求從 Model 層中獲取數(shù)據(jù),然后將數(shù)據(jù)返回給 View 層。

在 MVP 的架構(gòu)中,最大的特點(diǎn)就是 View 與 Model 之間的解耦,兩者之間必須通過 Presenter 來進(jìn)行通信,使得視圖和數(shù)據(jù)之間的關(guān)系變得完全分離。但是 View 和 Presenter 兩者之間的通信并不是想怎么調(diào)用就可以怎么調(diào)用的,下面講一下 MVP 模式最基本的實(shí)現(xiàn)方式。

MVP 基本的實(shí)現(xiàn)方式

  • 創(chuàng)建 IPresenter 接口(接口或類名自己定義,一般有約定成俗的寫法),把所有業(yè)務(wù)邏輯的接口都放在這里,并創(chuàng)建它的實(shí)現(xiàn)類 PresenterImpl。
  • 創(chuàng)建 IView 接口,把所有視圖邏輯的接口都放在這里,其實(shí)現(xiàn)類是Activity/Fragment。
  • 在 Activity/Fragment 中包含了一個(gè) IPresenter 的實(shí)例,而 PresenterImpl 里又包含了一個(gè) IView 的實(shí)例并且依賴了 Model。Activity/Fragment 只保留對(duì) IPresenter 的調(diào)用,當(dāng) View 層發(fā)生某些請(qǐng)求響應(yīng)或者生命周期發(fā)生變化,則會(huì)迅速的向 Presenter 層發(fā)起請(qǐng)求,讓 Presenter 做出相應(yīng)的處理。
  • Model 并不是必須有的,但是一定會(huì)有 View 和 Presenter。

我們還是以上面的功能為例,用 MVP 模式具體實(shí)現(xiàn)它。

IPresenter 接口:

public interface IPresenter {

    /**
     * 調(diào)用該方法表示 Presenter 被激活了
     */
    void start();

    void onBtnClick(int height, float weight);

    /**
     * 調(diào)用該方法表示 Presenter 要結(jié)束了
     * 為了避免相互持有引用而導(dǎo)致的內(nèi)存泄露
     */
    void destroy();

}

IView 接口:

public interface IView {

    /**
     * 用來更改按鈕的文本
     *
     * @param text
     */
    void updateBtnText(String text);

    /**
     * 用來彈出吐司顯示 BMI
     *
     * @param bmi
     */
    void showToast(float bmi);

}

IPresenter 接口的實(shí)現(xiàn)類 PresenterImpl:

public class PresenterImpl implements IPresenter {

    private IView mView;

    public PresenterImpl(IView mView) {
        this.mView = mView;
    }

    @Override
    public void start() {
        String text = User.instance().getBtnText();
        mView.updateBtnText(text);
    }

    @Override
    public void onBtnClick(int height, float weight) {
        User.instance().setHeight(height);
        User.instance().setWeight(weight);
        float bmi = User.instance().getBMI();
        mView.showToast(bmi);
    }

    @Override
    public void destroy() {
        mView = null;
    }
}

IView 接口的實(shí)現(xiàn)類 MVPActivity:

public class MVPActivity extends AppCompatActivity implements IView, View.OnClickListener {

    private EditText mEtHeight;
    private EditText mEtWeight;
    private Button mBtnCal;

    private IPresenter mPresenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 實(shí)例化 PresenterImpl
        mPresenter = new PresenterImpl(this);
        // View 的相關(guān)初始化
        mEtHeight = findViewById(R.id.et_height);
        mEtWeight = findViewById(R.id.et_weight);
        mBtnCal = findViewById(R.id.btn_cal);
        mBtnCal.setOnClickListener(this);
    }

    @Override
    protected void onStart() {
        super.onStart();
        mPresenter.start();
    }

    @Override
    public void onClick(View v) {
        int height = Integer.parseInt(mEtHeight.getText().toString());
        float weight = Float.parseFloat(mEtWeight.getText().toString());
        mPresenter.onBtnClick(height, weight);
    }

    @Override
    public void updateBtnText(String text) {
        mBtnCal.setText(text);
    }

    @Override
    public void showToast(float bmi) {
        Toast.makeText(this, "BMI: " + bmi, Toast.LENGTH_LONG).show();
    }

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

Model 層的代碼與 MVC 例子中的相同,這里就不再帖代碼了。

看完代碼可能有的人會(huì)發(fā)現(xiàn),相對(duì)于 MVC 模式來說,代碼不僅沒有減少,反而還增加了許多接口,看起來有些暈。但我們仔細(xì)觀察可以發(fā)現(xiàn),雖然增加了許多接口,但是 MVP 的結(jié)構(gòu)是非常清晰的,也是有很大的好處的,下面我們仔細(xì)分析一下。

MVPActivity 實(shí)現(xiàn)了IView 接口,并實(shí)現(xiàn)了 updateBtnText(..)showToast(..) 這兩個(gè)方法,但是這兩個(gè)方法看起來好像都沒有被調(diào)用,只是在 onCreate() 的時(shí)候創(chuàng)建了一個(gè) PresenterImpl 對(duì)象,在 onStart() 的時(shí)候調(diào)用了 mPresenter.start() 方法,然后在 onDestroy() 的時(shí)候調(diào)用了 mPresenter.destroy() 方法,而當(dāng)按鈕的點(diǎn)擊事件響應(yīng)的時(shí)候又調(diào)用了 mPresenter.onBtnClick(..) 方法,那么既沒有回調(diào)也沒有直接調(diào)用,那 IView 中的兩個(gè)接口方法又是何時(shí)何地被調(diào)用的呢?接下來我們將繼續(xù)分析 Presenter 層的實(shí)現(xiàn)代碼。

在 PresenterImpl 中實(shí)現(xiàn)了 IPresenter 接口并實(shí)現(xiàn)了 start() onBtnClick(..) destroy() 方法,在構(gòu)造方法中有一個(gè)IView的參數(shù),這個(gè)對(duì)象是 IView 的引用,這個(gè)對(duì)象可以是 Activity 或者是 Fragment 也可以是 IView 接口的任何一個(gè)實(shí)現(xiàn)類,但對(duì)于 PresenterImpl 而言具體的 IView 到底是誰并不知道。在 PresenterImpl 中,在 start()onBtnClick() 方法中除了調(diào)用 Model 外都調(diào)用了 IView 的方法:mView.updateBtnText(..)
mView.showToast(..),以此來對(duì) View 層的 UI 呈現(xiàn)以及交互提醒做出相應(yīng)的響應(yīng)。而最后的 destroy() 方法則是用于釋放對(duì) IView 的引用。

由此我們可以得出幾個(gè)結(jié)論:

對(duì)于 View 而言:

  • 我需要一位主持者,當(dāng)出現(xiàn)視圖相關(guān)事件的響應(yīng)或者生命周期的變化時(shí),我需要告訴這位主持者,我想要做些什么。
  • 我會(huì)提供一系列通用接口,以便于當(dāng)主持者完成我的請(qǐng)求后,調(diào)用相應(yīng)的接口告訴我這件事的結(jié)果。
  • 我所有的請(qǐng)求都發(fā)給主持者,讓他幫我做決定,但是這件事是怎么做的,我并不知道也不關(guān)心,我只是需要結(jié)果。

對(duì)于 Presenter 而言:

  • 我接收到 View 的請(qǐng)求后找 Model 尋求幫助,等 Model 做完事情后通知我了,我在把結(jié)果告訴 View。
  • 我只知道指揮 Model做事、告訴 View 顯示數(shù)據(jù),但我不干活。
  • 我相當(dāng)于一座橋,連接著 View 和 Model,他們誰也不認(rèn)識(shí)誰,想要通信必須要通過我,如果沒有我,他們兩永遠(yuǎn)都不會(huì)認(rèn)識(shí)。沒錯(cuò),我就是這么重要。

由于有 Presenter 的存在,View 層的代碼看起來是非常清晰的,每一個(gè)方法都有它自己的功能職責(zé),彼此之間并不會(huì)相互耦合。而 Presenter 中的代碼也是如此,每一個(gè)方法都只處理一件事,并不會(huì)做其他無相關(guān)的事情。另外我們觀察到,在 MVPActivity 中并沒有直接對(duì) PresenterImpl 進(jìn)行持有,而是持有了一個(gè) IPresenter 對(duì)象;同樣的在 PresenterImpl 也并沒有直接持有 MVPActivity 而是持有了一個(gè) IView 對(duì)象。也就是說,凡是實(shí)現(xiàn)了 IPresenter 便是 Presenter 層,凡是實(shí)現(xiàn)了 IView 便是 View 層,這樣就能很方便地變更業(yè)務(wù)邏輯或者進(jìn)行單元測試。下面就講一講 MVP 的優(yōu)勢與不足。

MVP 的優(yōu)勢與不足

優(yōu)勢:

  • 解耦,抽這么多接口出來就是為了解耦,非常適合多人協(xié)同開發(fā)。
  • 各模塊分工明確,結(jié)構(gòu)清晰。在 MVC 模式中,Activity/Fragment 兼顧著 Controller 與 View 的作用,雜亂且難以維護(hù),而 MVP 模式大大減少了 Activity/Fragment 的代碼,容易看懂、容易維護(hù)和修改。
  • 方便地變更業(yè)務(wù)邏輯。比如有三個(gè)功能,它們的 View 層完全一致,只是各自的業(yè)務(wù)邏輯不同,那么我們可以分別創(chuàng)建三個(gè)不同的 PresenterImpl (當(dāng)然他們都要實(shí)現(xiàn) IPresenter 接口),然后在 Activity 中創(chuàng)建 IPresenter 對(duì)象的時(shí)候,就可以根據(jù)不同的外部條件創(chuàng)建出不同的 PresenterImpl,這樣就能方便的實(shí)現(xiàn)它們各自的業(yè)務(wù)。
  • 方便進(jìn)行單元測試。由于業(yè)務(wù)邏輯都是在 IPresenter 中實(shí)現(xiàn)的,那么我們可以創(chuàng)建一個(gè) PresenterTest 實(shí)現(xiàn) IPresenter 接口,然后把 MVPActivity 中對(duì) PresenterImpl 的創(chuàng)建改成 PresenterTest 的創(chuàng)建,然后就可以對(duì) IView 的方法隨意進(jìn)行測試了。如果想要測試 IPresenter 中的方法,那就新建一個(gè) ViewTest 類實(shí)現(xiàn) IView 接口,然后將其傳入 PresenterImpl,便可以自由的測試 IPresenter 中的方法是否有效。
  • 避免 Activity 內(nèi)存泄露。Activity 是有生命周期的,用戶隨時(shí)可能切換 Activity,當(dāng) APP 的內(nèi)存不夠用的時(shí)候,系統(tǒng)會(huì)回收處于后臺(tái)的 Activity 的資源以避免 OOM。采用傳統(tǒng)的模式,一大堆異步任務(wù)都有可能保留著對(duì) Activity 的引用,比如說許多圖片加載框架。這樣一來,即使 Activity 的 onDestroy() 已經(jīng)執(zhí)行,這些 異步任務(wù)仍然保留著對(duì) Activity 實(shí)例的引用, 所以系統(tǒng)就無法回收這個(gè) Activity 實(shí)例了,結(jié)果就是 Activity Leak。Android 的組件中,Activity 對(duì)象往往是在堆里占最多內(nèi)存的,所以系統(tǒng)會(huì)優(yōu)先回收 Activity 對(duì)象, 如果有 Activity Leak,APP很容易因?yàn)閮?nèi)存不夠而 OOM。采用 MVP 模式,只要在當(dāng)前的 Activity 的 onDestroy() 里,分離異步任務(wù)對(duì) Activity 的引用,就能避免 Activity Leak。

不足:

  • 有點(diǎn)笨重,不適合短期小型的項(xiàng)目開發(fā)。你一個(gè) Activity 就能搞定的事,非要用 MVP 干嘛。
  • 雖然 Activity 變得輕松了,但是 Presenter 的業(yè)務(wù)越來越復(fù)雜。
  • 提高了學(xué)習(xí)成本,由于 MVP 的變種非常多,需要自己在實(shí)戰(zhàn)中慢慢摸索。

補(bǔ)充

  1. 關(guān)于 MVP 的分包結(jié)構(gòu),有的人習(xí)慣按照下面這種方式分包:

將所有的 Model/View/Presenter 的代碼分別放在同一個(gè)包下,這樣業(yè)務(wù)多了會(huì)很亂。也有人喜歡按照模塊分包,將同一個(gè)功能模塊的 Model/View/Presenter 放在一個(gè)模塊包下。具體的分包方式還是要按照具體的項(xiàng)目和自己的喜好來定。

  1. 在使用上述 MVP 模式進(jìn)行開發(fā)的過程中,還遇到了空指針的問題。當(dāng) Presenter 中通過異步方式獲取數(shù)據(jù)然后需要更新 View 的時(shí)候,這個(gè)時(shí)候 View 有可能已經(jīng)消失了,極度容易引起 NullPointerException。比如下面的示例代碼:
@Override
public void login(String phone, String pwd) {
    OkGo.<BaseModal<User>>get(url).tag(this)
            .params(AppInterface.getLoginParams(phone, pwd))
            .execute(new JsonCallback<BaseModal<User>>() {
                @Override
                public void onSuccess(Response<BaseModal<User>> response) {
                    if (mView == null) {
                        return;
                    }
                    mView.showToast("登錄成功");
                }

                @Override
                public void onError(Response<BaseModal<User>> response) {
                    if (mView == null) {
                        return;
                    }
                    mView.showToast("登錄失敗");
                }
            });
}

由上面的代碼可以看出,在 Presenter 進(jìn)行異步回調(diào)后,一定要對(duì) mView 進(jìn)行非空判斷,否則會(huì)出現(xiàn)大面積的 NullPointerException。

總結(jié)

以上就是 MVP 模式基本的實(shí)現(xiàn)方式,可能示例代碼太簡單無法體現(xiàn) MVP 的優(yōu)勢,但是真正地理解了它并在項(xiàng)目中實(shí)際使用,你便能體會(huì)到它所帶來的好處。MVP 有很多變種與改進(jìn),網(wǎng)上也有很多資料,如果想學(xué)的話,可以很方便地找到。另外,Google 官方也開源了一系列 Andorid 架構(gòu)的使用示例,其中就包括了 MVP 模式,地址:https://github.com/googlesamples/android-architecture 。

本篇博客示例代碼:https://github.com/ayuhani/mvp_demo

歡迎關(guān)注我的微信公眾號(hào)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容