文章已同步至 CSDN:http://blog.csdn.net/qq_24867873/article/details/79459856
前言
記得自己接手的第二個(gè)項(xiàng)目采用的是 MVP 模式進(jìn)行開(kāi)發(fā)的,當(dāng)時(shí)架構(gòu)已經(jīng)設(shè)計(jì)好派桩,我看了幾篇關(guān)于 MVP 的文章构诚,對(duì)其有了基本的了解之后,便照貓畫(huà)虎進(jìn)行了開(kāi)發(fā)铆惑,之后便再也沒(méi)接觸過(guò) MVP范嘱。
最近空閑的時(shí)候讀了一篇 MVP 相關(guān)的文章送膳,受益匪淺。于是打算寫(xiě)一篇關(guān)于它的文章丑蛤,一方面是作為自己的學(xué)習(xí)筆記方便查看叠聋,另一反面希望能給沒(méi)有接觸過(guò) MVP 模式的新人提供幫助,以便可以快速入門(mén)受裹。
什么是 MVC
在講 MVP 之前碌补,我們先來(lái)了解一下 MVC。
MVC 模式是經(jīng)典的三層架構(gòu)一種具體的實(shí)現(xiàn)方式棉饶,全稱為 Model(模型層) 厦章、View(視圖層)、Controller(控制器)照藻。下面介紹一下它們各自的職責(zé):
- Model 層:用來(lái)定義實(shí)體對(duì)象袜啃,處理業(yè)務(wù)邏輯,可以簡(jiǎn)單地理解成 Java 中的實(shí)體類幸缕。
- View 層:負(fù)責(zé)處理界面的顯示群发,在 Android 中對(duì)應(yīng)的就是 xml 文件。
- Controller 層:對(duì)應(yīng)的是 Activity/Fragment 发乔,當(dāng)加載完成 xml 布局之后熟妓,我們需要找到并設(shè)置布局中的各個(gè) View,處理用戶的交互事件列疗,更新 View 等滑蚯。
下面我們通過(guò)一個(gè)簡(jiǎn)單的例子來(lái)說(shuō)明這三者是如何交互的。
首先是 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 訪問(wèn)了 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)聽(tīng)器
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 又訪問(wèn)了 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ù)庫(kù)中查詢數(shù)據(jù)
// 或者訪問(wèn)網(wǎng)絡(luò)獲取數(shù)據(jù)
return "計(jì)算BMI";
}
public float getBMI() {
// 通過(guò)已有的屬性計(jì)算出新的屬性,也屬于業(yè)務(wù)邏輯的操作
return weight / (height * height) * 10000;
}
}
從上面的代碼中缰猴,我們可以看到 View 層的職責(zé)是非常簡(jiǎn)單的产艾,向用戶呈現(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 來(lái)說(shuō),它不僅僅是個(gè)簡(jiǎn)單的實(shí)體類佛点,還應(yīng)該包括數(shù)據(jù)處理與業(yè)務(wù)邏輯的操作醇滥,比如說(shuō)對(duì)數(shù)據(jù)庫(kù)的操作、網(wǎng)絡(luò)請(qǐng)求等超营,但是很多情況下鸳玩,我們很少把這些操作寫(xiě)在實(shí)體類中。
demo 運(yùn)行效果如下:
在 MVC 模式中糟描,Controller 層扮演著重要的角色怀喉,它不僅要處理 UI 的顯示與事件的響應(yīng),還要負(fù)責(zé)與 Model 層的通信船响,同時(shí) Model 層與 View 層也會(huì)通信躬拢,三者的耦合度很大。
作為 Android 開(kāi)發(fā)中默認(rèn)使用的架構(gòu)模式见间,MVC 易于上手聊闯,適合快速開(kāi)發(fā)一些小型項(xiàng)目。但是隨著業(yè)務(wù)邏輯的復(fù)雜度越來(lái)越大米诉,Activity/Fragment 會(huì)越來(lái)越臃腫菱蔬,因?yàn)樗瑫r(shí)承擔(dān)著 Controller 與 View 的角色,這對(duì)于項(xiàng)目后期的更新維護(hù)與測(cè)試交接都是非常不方便的史侣,大大提高了生產(chǎn)成本拴泌。這么一來(lái),它就違背了 “提高生產(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 層是一樣的觅廓,這里不再說(shuō)了鼻忠。
- 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 之間的解耦棘催,兩者之間必須通過(guò) Presenter 來(lái)進(jìn)行通信,使得視圖和數(shù)據(jù)之間的關(guān)系變得完全分離耳标。但是 View 和 Presenter 兩者之間的通信并不是想怎么調(diào)用就可以怎么調(diào)用的醇坝,下面講一下 MVP 模式最基本的實(shí)現(xiàn)方式。
MVP 基本的實(shí)現(xiàn)方式
- 創(chuàng)建 IPresenter 接口(接口或類名自己定義次坡,一般有約定成俗的寫(xiě)法)呼猪,把所有業(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 {
/**
* 用來(lái)更改按鈕的文本
*
* @param text
*/
void updateBtnText(String text);
/**
* 用來(lái)彈出吐司顯示 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 模式來(lái)說(shuō)茴肥,代碼不僅沒(méi)有減少坚踩,反而還增加了許多接口,看起來(lái)有些暈瓤狐。但我們仔細(xì)觀察可以發(fā)現(xiàn)瞬铸,雖然增加了許多接口批幌,但是 MVP 的結(jié)構(gòu)是非常清晰的,也是有很大的好處的嗓节,下面我們仔細(xì)分析一下荧缘。
MVPActivity 實(shí)現(xiàn)了IView 接口,并實(shí)現(xiàn)了 updateBtnText(..)
和showToast(..)
這兩個(gè)方法拦宣,但是這兩個(gè)方法看起來(lái)好像都沒(méi)有被調(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(..)
方法,那么既沒(méi)有回調(diào)也沒(méi)有直接調(diào)用豆瘫,那 IView 中的兩個(gè)接口方法又是何時(shí)何地被調(diào)用的呢珊蟀?接下來(lái)我們將繼續(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 到底是誰(shuí)并不知道描扯。在 PresenterImpl 中,在 start()
和 onBtnClick()
方法中除了調(diào)用 Model 外都調(diào)用了 IView 的方法:mView.updateBtnText(..)
和 mView.showToast(..)
趟薄,以此來(lái)對(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人弓,他們誰(shuí)也不認(rèn)識(shí)誰(shuí)沼死,想要通信必須要通過(guò)我,如果沒(méi)有我票从,他們兩永遠(yuǎn)都不會(huì)認(rèn)識(shí)漫雕。沒(méi)錯(cuò),我就是這么重要峰鄙。
由于有 Presenter 的存在浸间,View 層的代碼看起來(lái)是非常清晰的,每一個(gè)方法都有它自己的功能職責(zé)吟榴,彼此之間并不會(huì)相互耦合魁蒜。而 Presenter 中的代碼也是如此,每一個(gè)方法都只處理一件事吩翻,并不會(huì)做其他無(wú)相關(guān)的事情兜看。另外我們觀察到,在 MVPActivity 中并沒(méi)有直接對(duì) PresenterImpl 進(jìn)行持有狭瞎,而是持有了一個(gè) IPresenter 對(duì)象细移;同樣的在 PresenterImpl 也并沒(méi)有直接持有 MVPActivity 而是持有了一個(gè) IView 對(duì)象。也就是說(shuō)熊锭,凡是實(shí)現(xiàn)了 IPresenter 便是 Presenter 層弧轧,凡是實(shí)現(xiàn)了 IView 便是 View 層,這樣就能很方便地變更業(yè)務(wù)邏輯或者進(jìn)行單元測(cè)試碗殷。下面就講一講 MVP 的優(yōu)勢(shì)與不足精绎。
MVP 的優(yōu)勢(shì)與不足
優(yōu)勢(shì):
- 解耦,抽這么多接口出來(lái)就是為了解耦锌妻,非常適合多人協(xié)同開(kāi)發(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)行單元測(cè)試银伟。由于業(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)行測(cè)試了傅物。如果想要測(cè)試 IPresenter 中的方法,那就新建一個(gè) ViewTest 類實(shí)現(xiàn) IView 接口琉预,然后將其傳入 PresenterImpl董饰,便可以自由的測(cè)試 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 的引用近速,比如說(shuō)許多圖片加載框架诈嘿。這樣一來(lái),即使 Activity 的 onDestroy() 已經(jīng)執(zhí)行数焊,這些 異步任務(wù)仍然保留著對(duì) Activity 實(shí)例的引用永淌, 所以系統(tǒng)就無(wú)法回收這個(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)目開(kāi)發(fā)。你一個(gè) Activity 就能搞定的事,非要用 MVP 干嘛状土。
- 雖然 Activity 變得輕松了无蜂,但是 Presenter 的業(yè)務(wù)越來(lái)越復(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)目和自己的喜好來(lái)定。
- 在使用上述 MVP 模式進(jìn)行開(kāi)發(fā)的過(guò)程中毒租,還遇到了空指針的問(wèn)題稚铣。當(dāng) Presenter 中通過(guò)異步方式獲取數(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)方式灾梦,可能示例代碼太簡(jiǎn)單無(wú)法體現(xiàn) MVP 的優(yōu)勢(shì)峡钓,但是真正地理解了它并在項(xiàng)目中實(shí)際使用,你便能體會(huì)到它所帶來(lái)的好處若河。MVP 有很多變種與改進(jìn)能岩,網(wǎng)上也有很多資料,如果想學(xué)的話萧福,可以很方便地找到拉鹃。另外,Google 官方也開(kāi)源了一系列 Andorid 架構(gòu)的使用示例鲫忍,其中就包括了 MVP 模式膏燕,地址:https://github.com/googlesamples/android-architecture 。
本篇博客示例代碼:https://github.com/ayuhani/mvp_demo