Android MVP 模式解析與基本實現方式
前言
記得自己接手的第二個專案採用的是 MVP 模式進行開發的,當時架構已經設計好,我看了幾篇關於 MVP 的文章,對其有了基本的瞭解之後,便照貓畫虎進行了開發,之後便再也沒接觸過 MVP。
最近空閒的時候讀了一篇 MVP 相關的文章,受益匪淺。於是打算寫一篇關於它的文章,一方面是作為自己的學習筆記方便檢視,另一反面希望能給沒有接觸過 MVP 模式的新人提供幫助,以便可以快速入門。
什麼是 MVC
在講 MVP 之前,我們先來了解一下 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 是 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 。