文章已同步至 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 模式是經(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)行效果如下:
在 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 是 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ǔ)充
- 關(guān)于 MVP 的分包結(jié)構(gòu),有的人習(xí)慣按照下面這種方式分包:
將所有的 Model/View/Presenter 的代碼分別放在同一個(gè)包下,這樣業(yè)務(wù)多了會(huì)很亂。也有人喜歡按照模塊分包,將同一個(gè)功能模塊的 Model/View/Presenter 放在一個(gè)模塊包下。具體的分包方式還是要按照具體的項(xiàng)目和自己的喜好來定。
- 在使用上述 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