1. 程式人生 > >Android MVP 模式解析與基本實現方式

Android MVP 模式解析與基本實現方式

前言

記得自己接手的第二個專案採用的是 MVP 模式進行開發的,當時架構已經設計好,我看了幾篇關於 MVP 的文章,對其有了基本的瞭解之後,便照貓畫虎進行了開發,之後便再也沒接觸過 MVP。

最近空閒的時候讀了一篇 MVP 相關的文章,受益匪淺。於是打算寫一篇關於它的文章,一方面是作為自己的學習筆記方便檢視,另一反面希望能給沒有接觸過 MVP 模式的新人提供幫助,以便可以快速入門。

什麼是 MVC

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

MVC 結構圖

MVC 模式是經典的三層架構一種具體的實現方式,全稱為 Model(模型層) 、View(檢視層)、Controller(控制器)。下面介紹一下它們各自的職責:

  • Model 層:用來定義實體物件,處理業務邏輯,可以簡單地理解成 Java 中的實體類。
  • View 層:負責處理介面的顯示,在 Android 中對應的就是 xml 檔案。
  • Controller 層:對應的是 Activity/Fragment ,當載入完成 xml 佈局之後,我們需要找到並設定佈局中的各個 View,處理使用者的互動事件,更新 View 等。

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

首先是 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);
        // 這個點選事件屬於 View,它是 View 的監聽器
        mBtnCal.setOnClickListener(this);

        // Controller 呼叫了 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 中的資料
        User.instance().setHeight(height);
        User.instance().setWeight(weight);
        // 這裡 View 又訪問了 Model 的資料,並呈現在 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() {
        // 在這裡,我們可以從資料庫中查詢資料
        // 或者訪問網路獲取資料
        return "計算BMI";
    }

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

從上面的程式碼中,我們可以看到 View 層的職責是非常簡單的,向用戶呈現 xml 檔案中的佈局,並且響應使用者的觸控事件。

而 Controller 層的職責邏輯則複雜很多。它對於 View 層,需要將從 Model 中獲取到的資料及時地呈現在 UI 上。而對於 Model 層,當 app 的生命週期發生變化或者接收到某些響應時,需要對 Model 的資料進行 CRUD。在這個例子中,使用者點選按鈕的時候,首先獲取 View 層使用者的輸入,然後更新 Model 層的屬性,最後獲取到 Model 層計算得出的新資料並顯示在 UI 上。

對於 Model 來說,它不僅僅是個簡單的實體類,還應該包括資料處理與業務邏輯的操作,比如說對資料庫的操作、網路請求等,但是很多情況下,我們很少把這些操作寫在實體類中。

demo 執行效果如下:

執行效果

在 MVC 模式中,Controller 層扮演著重要的角色,它不僅要處理 UI 的顯示與事件的響應,還要負責與 Model 層的通訊,同時 Model 層與 View 層也會通訊,三者的耦合度很大。

作為 Android 開發中預設使用的架構模式,MVC 易於上手,適合快速開發一些小型專案。但是隨著業務邏輯的複雜度越來越大,Activity/Fragment 會越來越臃腫,因為它同時承擔著 Controller 與 View 的角色,這對於專案後期的更新維護與測試交接都是非常不方便的,大大提高了生產成本。這麼一來,它就違背了 “提高生產力” 的初衷,於是 MVP 模式就應運而生了。

什麼是 MVP

MVP 結構圖

MVP 是 MVC 的一種升級進化,全稱為 Model(模型層)、View(檢視層)、Presenter(主持者)。從結構圖中,我們可以看到它與 MVC 的區別:Presenter 代替了 Controller,去除了 View 與 Model 的關聯與耦合。

  • Model 層:和 MVC 模式中的 Model 層是一樣的,這裡不再說了。
  • View 層:檢視層。在 MVP 中,它不僅僅對應 xml 佈局了,Activity/Fragment 也屬於檢視層。View 層現在不僅作為 UI 的顯示,還負責響應生命週期的變化。
  • Presenter 層:主持者層,是 Model 層與 View 層進行溝通的橋樑,處理業務邏輯。它響應 View 層的請求從 Model 層中獲取資料,然後將資料返回給 View 層。

在 MVP 的架構中,最大的特點就是 View 與 Model 之間的解耦,兩者之間必須通過 Presenter 來進行通訊,使得檢視和資料之間的關係變得完全分離。但是 View 和 Presenter 兩者之間的通訊並不是想怎麼呼叫就可以怎麼呼叫的,下面講一下 MVP 模式最基本的實現方式。

MVP 基本的實現方式

  • 建立 IPresenter 介面(介面或類名自己定義,一般有約定成俗的寫法),把所有業務邏輯的介面都放在這裡,並建立它的實現類 PresenterImpl。
  • 建立 IView 介面,把所有檢視邏輯的介面都放在這裡,其實現類是Activity/Fragment。
  • 在 Activity/Fragment 中包含了一個 IPresenter 的例項,而 PresenterImpl 裡又包含了一個 IView 的例項並且依賴了 Model。Activity/Fragment 只保留對 IPresenter 的呼叫,當 View 層發生某些請求響應或者生命週期發生變化,則會迅速的向 Presenter 層發起請求,讓 Presenter 做出相應的處理。
  • Model 並不是必須有的,但是一定會有 View 和 Presenter。

我們還是以上面的功能為例,用 MVP 模式具體實現它。

IPresenter 介面:

public interface IPresenter {

    /**
     * 呼叫該方法表示 Presenter 被激活了
     */
    void start();

    void onBtnClick(int height, float weight);

    /**
     * 呼叫該方法表示 Presenter 要結束了
     * 為了避免相互持有引用而導致的記憶體洩露
     */
    void destroy();

}

IView 介面:

public interface IView {

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

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

}

IPresenter 介面的實現類 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 介面的實現類 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);

        // 例項化 PresenterImpl
        mPresenter = new PresenterImpl(this);
        // View 的相關初始化
        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 例子中的相同,這裡就不再帖程式碼了。

看完程式碼可能有的人會發現,相對於 MVC 模式來說,程式碼不僅沒有減少,反而還增加了許多介面,看起來有些暈。但我們仔細觀察可以發現,雖然增加了許多介面,但是 MVP 的結構是非常清晰的,也是有很大的好處的,下面我們仔細分析一下。

MVPActivity 實現了IView 介面,並實現了 updateBtnText(..)showToast(..) 這兩個方法,但是這兩個方法看起來好像都沒有被呼叫,只是在 onCreate() 的時候建立了一個 PresenterImpl 物件,在 onStart() 的時候呼叫了 mPresenter.start() 方法,然後在 onDestroy() 的時候呼叫了 mPresenter.destroy() 方法,而當按鈕的點選事件響應的時候又呼叫了 mPresenter.onBtnClick(..) 方法,那麼既沒有回撥也沒有直接呼叫,那 IView 中的兩個介面方法又是何時何地被呼叫的呢?接下來我們將繼續分析 Presenter 層的實現程式碼。

在 PresenterImpl 中實現了 IPresenter 介面並實現了 start() onBtnClick(..) destroy() 方法,在構造方法中有一個IView的引數,這個物件是 IView 的引用,這個物件可以是 Activity 或者是 Fragment 也可以是 IView 介面的任何一個實現類,但對於 PresenterImpl 而言具體的 IView 到底是誰並不知道。在 PresenterImpl 中,在 start()onBtnClick() 方法中除了呼叫 Model 外都呼叫了 IView 的方法:mView.updateBtnText(..)
mView.showToast(..),以此來對 View 層的 UI 呈現以及互動提醒做出相應的響應。而最後的 destroy() 方法則是用於釋放對 IView 的引用。

由此我們可以得出幾個結論:

對於 View 而言:

  • 我需要一位主持者,當出現檢視相關事件的響應或者生命週期的變化時,我需要告訴這位主持者,我想要做些什麼。
  • 我會提供一系列通用介面,以便於當主持者完成我的請求後,呼叫相應的介面告訴我這件事的結果。
  • 我所有的請求都發給主持者,讓他幫我做決定,但是這件事是怎麼做的,我並不知道也不關心,我只是需要結果。

對於 Presenter 而言:

  • 我接收到 View 的請求後找 Model 尋求幫助,等 Model 做完事情後通知我了,我在把結果告訴 View。
  • 我只知道指揮 Model做事、告訴 View 顯示資料,但我不幹活。
  • 我相當於一座橋,連線著 View 和 Model,他們誰也不認識誰,想要通訊必須要通過我,如果沒有我,他們兩永遠都不會認識。沒錯,我就是這麼重要。

由於有 Presenter 的存在,View 層的程式碼看起來是非常清晰的,每一個方法都有它自己的功能職責,彼此之間並不會相互耦合。而 Presenter 中的程式碼也是如此,每一個方法都只處理一件事,並不會做其他無相關的事情。另外我們觀察到,在 MVPActivity 中並沒有直接對 PresenterImpl 進行持有,而是持有了一個 IPresenter 物件;同樣的在 PresenterImpl 也並沒有直接持有 MVPActivity 而是持有了一個 IView 物件。也就是說,凡是實現了 IPresenter 便是 Presenter 層,凡是實現了 IView 便是 View 層,這樣就能很方便地變更業務邏輯或者進行單元測試。下面就講一講 MVP 的優勢與不足。

MVP 的優勢與不足

優勢:

  • 解耦,抽這麼多接口出來就是為了解耦,非常適合多人協同開發。
  • 各模組分工明確,結構清晰。在 MVC 模式中,Activity/Fragment 兼顧著 Controller 與 View 的作用,雜亂且難以維護,而 MVP 模式大大減少了 Activity/Fragment 的程式碼,容易看懂、容易維護和修改。
  • 方便地變更業務邏輯。比如有三個功能,它們的 View 層完全一致,只是各自的業務邏輯不同,那麼我們可以分別建立三個不同的 PresenterImpl (當然他們都要實現 IPresenter 介面),然後在 Activity 中建立 IPresenter 物件的時候,就可以根據不同的外部條件創建出不同的 PresenterImpl,這樣就能方便的實現它們各自的業務。
  • 方便進行單元測試。由於業務邏輯都是在 IPresenter 中實現的,那麼我們可以建立一個 PresenterTest 實現 IPresenter 介面,然後把 MVPActivity 中對 PresenterImpl 的建立改成 PresenterTest 的建立,然後就可以對 IView 的方法隨意進行測試了。如果想要測試 IPresenter 中的方法,那就新建一個 ViewTest 類實現 IView 介面,然後將其傳入 PresenterImpl,便可以自由的測試 IPresenter 中的方法是否有效。
  • 避免 Activity 記憶體洩露。Activity 是有生命週期的,使用者隨時可能切換 Activity,當 APP 的記憶體不夠用的時候,系統會回收處於後臺的 Activity 的資源以避免 OOM。採用傳統的模式,一大堆非同步任務都有可能保留著對 Activity 的引用,比如說許多圖片載入框架。這樣一來,即使 Activity 的 onDestroy() 已經執行,這些 非同步任務仍然保留著對 Activity 例項的引用, 所以系統就無法回收這個 Activity 例項了,結果就是 Activity Leak。Android 的元件中,Activity 物件往往是在堆裡佔最多記憶體的,所以系統會優先回收 Activity 物件, 如果有 Activity Leak,APP很容易因為記憶體不夠而 OOM。採用 MVP 模式,只要在當前的 Activity 的 onDestroy() 裡,分離非同步任務對 Activity 的引用,就能避免 Activity Leak。

不足:

  • 有點笨重,不適合短期小型的專案開發。你一個 Activity 就能搞定的事,非要用 MVP 幹嘛。
  • 雖然 Activity 變得輕鬆了,但是 Presenter 的業務越來越複雜。
  • 提高了學習成本,由於 MVP 的變種非常多,需要自己在實戰中慢慢摸索。

補充

1.關於 MVP 的分包結構,有的人習慣按照下面這種方式分包:

將所有的 Model/View/Presenter 的程式碼分別放在同一個包下,這樣業務多了會很亂。也有人喜歡按照模組分包,將同一個功能模組的 Model/View/Presenter 放在一個模組包下。具體的分包方式還是要按照具體的專案和自己的喜好來定。

2.在使用上述 MVP 模式進行開發的過程中,還遇到了空指標的問題。當 Presenter 中通過非同步方式獲取資料然後需要更新 View 的時候,這個時候 View 有可能已經消失了,極度容易引起 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 進行非同步回撥後,一定要對 mView 進行非空判斷,否則會出現大面積的 NullPointerException。

總結

以上就是 MVP 模式基本的實現方式,可能示例程式碼太簡單無法體現 MVP 的優勢,但是真正地理解了它並在專案中實際使用,你便能體會到它所帶來的好處。MVP 有很多變種與改進,網上也有很多資料,如果想學的話,可以很方便地找到。另外,Google 官方也開源了一系列 Andorid 架構的使用示例,其中就包括了 MVP 模式,地址:https://github.com/googlesamples/android-architecture

歡迎關注我的微信公眾號
歡迎關注我的微信公眾號