一舱殿、什么是 MVP
1.1. MVP 的定義
MVP,全稱 Model-View-Presenter 模型-視圖-表示器
隨著UI創(chuàng)建技術(shù)的功能日益增強,UI層也履行著越來越多的職責(zé)。為了更好地細分視圖(View)與模型(Model)的功能,讓View專注于處理數(shù)據(jù)的可視化以及與用戶的交互,同時讓Model只關(guān)系數(shù)據(jù)的處理,基于MVC(Model-View-Controller)概念的MVP模式應(yīng)運而生。
1.2. 為什么需要 MVP
理由1:盡量簡單
大部分的安卓應(yīng)用只使用View-Model結(jié)構(gòu)
程序員現(xiàn)在更多的是和復(fù)雜的View打交道而不是解決業(yè)務(wù)邏輯抵蚊。
當你在應(yīng)用中只使用Model-View時,到最后,你會發(fā)現(xiàn)“所有的事物都被連接到一起”。
只使用 Model-View
如果這張圖看上去還不是很復(fù)雜斯够,那么請你想象一下以下情況:每一個View在任意一個時刻都有可能出現(xiàn)或者消失掖桦。不要忘記View的保存和恢復(fù)怔昨,在臨時的view上掛載一個后臺任務(wù)矮烹。
“所有的事物都被連接到一起”的替代品是一個萬能對象(god object)桑驱。
god object
god object是十分復(fù)雜的赊级,他的每一個部分都不能重復(fù)利用押框,無法輕易的測試、或者調(diào)試和重構(gòu)此衅。
那么MVP呢强戴?
使用 MVP
復(fù)雜的任務(wù)被分成細小的任務(wù)亭螟,并且很容易解決。越小的東西骑歹,bug越少预烙,越容易debug,更好測試道媚。在MVP模式下的View層將會變得簡單扁掸,所以即便是他請求數(shù)據(jù)的時候也不需要回調(diào)函數(shù)。View邏輯變成十分直接最域。
理由2:后臺任務(wù)
當你編寫一個Actviity谴分、Fragment、自定義View的時候镀脂,你會把所有的和后臺任務(wù)相關(guān)的方法寫在一個靜態(tài)類或者外部類中牺蹄。這樣,你的Task不再和Activity聯(lián)系在一起薄翅,這既不會導(dǎo)致內(nèi)存泄露沙兰,也不依賴于Activity的重建。
這里有若干種方法處理后臺任務(wù)翘魄,但是它們的可靠性都不及MVP鼎天。
1.3. MVP 的優(yōu)缺點
任何事務(wù)都存在兩面性,MVP當然也不列外暑竟,我們來看看MVP的優(yōu)缺點斋射。
優(yōu)點:
- 降低耦合度,實現(xiàn)了Model和View真正的完全分離但荤,可以修改View而不影響Modle
- 模塊職責(zé)劃分明顯罗岖,層次清晰(下面會介紹Bob大叔的Clean Architecture)
- 隱藏數(shù)據(jù)
- Presenter可以復(fù)用,一個Presenter可以用于多個View腹躁,而不需要更改Presenter的邏輯(當然是在View的改動不影響業(yè)務(wù)邏輯的前提下)
- 利于測試驅(qū)動開發(fā)呀闻。以前的Android開發(fā)是難以進行單元測試的(雖然很多Android開發(fā)者都沒有寫過測試用例,但是隨著項目變得越來越復(fù)雜潜慎,沒有測試是很難保證軟件質(zhì)量的捡多;而且近幾年來Android上的測試框架已經(jīng)有了長足的發(fā)展——開始寫測試用例吧),在使用MVP的項目中Presenter對View是通過接口進行铐炫,在對Presenter進行不依賴UI環(huán)境的單元測試的時候垒手。可以通過Mock一個View對象倒信,這個對象只需要實現(xiàn)了View的接口即可科贬。然后依賴注入到Presenter中,單元測試的時候就可以完整的測試Presenter應(yīng)用邏輯的正確性。
- View可以進行組件化榜掌。在MVP當中优妙,View不依賴Model。這樣就可以讓View從特定的業(yè)務(wù)場景中脫離出來憎账,可以說View可以做到對業(yè)務(wù)完全無知套硼。它只需要提供一系列接口提供給上層操作。這樣就可以做到高度可復(fù)用的View組件胞皱。
- 代碼靈活性
缺點: - Presenter中除了應(yīng)用邏輯以外邪意,還有大量的View->Model,Model->View的手動同步邏輯反砌,造成Presenter比較笨重雾鬼,維護起來會比較困難。
- 由于對視圖的渲染放在了Presenter中宴树,所以視圖和Presenter的交互會過于頻繁策菜。
- 如果Presenter過多地渲染了視圖,往往會使得它與特定的視圖的聯(lián)系過于緊密酒贬。一旦視圖需要變更做入,那么Presenter也需要變更了。
- 額外的代碼復(fù)雜度及學(xué)習(xí)成本同衣。
1.4. 小結(jié)
在MVP模式里通常包含4個要素:
(1) View:負責(zé)繪制UI元素、與用戶進行交互(在Android中體現(xiàn)為Activity);
(2) View interface:需要View實現(xiàn)的接口壶运,View通過View interface與Presenter進行交互耐齐,降低耦合,方便進行單元測試;
(3) Model :負責(zé)存儲蒋情、檢索埠况、操縱數(shù)據(jù)(有時也實現(xiàn)一個Model interface用來降低耦合);
(4) Presenter :作為View與Model交互的中間紐帶,處理與用戶交互的負責(zé)邏輯棵癣。
二辕翰、MVX 剖析
2.1. M(Model)
模型:表示數(shù)據(jù)模型和業(yè)務(wù)邏輯(business logic)。
model層主要負責(zé):
- 從網(wǎng)絡(luò)狈谊,數(shù)據(jù)庫喜命,文件,傳感器河劝,第三方等數(shù)據(jù)源讀寫數(shù)據(jù)壁榕。
- 對外部的數(shù)據(jù)類型進行解析轉(zhuǎn)換為APP內(nèi)部數(shù)據(jù)交由上層處理。
- 對數(shù)據(jù)的臨時存儲,管理赎瞎,協(xié)調(diào)上層數(shù)據(jù)請求牌里。
2.2 V(View)
視圖:將數(shù)據(jù)呈現(xiàn)給用戶。
view 層主要負責(zé):
- 提供UI交互
- 在presenter的控制下修改UI务甥。
- 將業(yè)務(wù)事件交由presenter處理牡辽。
注意: View層不存儲數(shù)據(jù)喳篇,不與Model層交互。
在Android中View層一般是Activity态辛、Fragment麸澜、View(控件)、ViewGroup(布局等)等因妙。
三痰憎、MVP與MVC的異同
MVC模式與MVP模式都作為用來分離UI層與業(yè)務(wù)層的一種開發(fā)模式被應(yīng)用了很多年。在我們選擇一種開發(fā)模式時攀涵,首先需要了解一下這種模式的利弊:
無論MVC或是MVP模式都不可避免地存在一個弊端:
額外的代碼復(fù)雜度及學(xué)習(xí)成本铣耘。
這就導(dǎo)致了這兩種開發(fā)模式也許并不是很小型應(yīng)用。
但比起他們的優(yōu)點以故,這點弊端基本可以忽略了:
(1)降低耦合度
(2)模塊職責(zé)劃分明顯
(3)利于測試驅(qū)動開發(fā)
(4)代碼復(fù)用
(5)隱藏數(shù)據(jù)
(6)代碼靈活性
對于MVP與MVC這兩種模式蜗细,它們之間也有很大的差異。有一些程序員選擇不使用任何一種模式怒详,有一部分原因也許就是不能區(qū)分這兩種模式差異炉媒。以下是這兩種模式之間最關(guān)鍵的差異:
MVP模式:
- View不直接與Model交互,而是通過與Presenter交互來與Model間接交互
- Presenter與View的交互是通過接口來進行的昆烁,更有利于添加單元測試
- 通常View與Presenter是一對一的吊骤,但復(fù)雜的View可能綁定多個Presenter來處理邏輯
MVC模式: - View可以與Model直接交互
- Controller是基于行為的,并且可以被多個View共享
- 可以負責(zé)決定顯示哪個View
四静尼、利用MVP進行Android開發(fā)的例子
說了這么多理論白粉,現(xiàn)在輪到實踐了。
現(xiàn)在我們來實現(xiàn)這樣一個Android上的Demo(如圖):可以從EditText讀取用戶信息并存取鼠渺,也可以根據(jù)ID來從后臺讀出用戶信息并顯示鸭巴。
頁面布局很簡單,就不介紹了拦盹。下面根據(jù)MVP原則來進行編碼:
先來看看java文件的目錄結(jié)構(gòu):
可以發(fā)現(xiàn)鹃祖,Presenter與Model、View都是通過接口來進行交互的普舆,既降低耦合也方便進行單元測試恬口。
(1)首先我們需要一個UserBean,用來保存用戶信息
package com.bbno.mvp2.bean;
public class UserBean {
private String firstName;
private String lastName;
public UserBean(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
@Override
public String toString() {
return "UserBean{" +
"firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
'}';
}
}
(2)再來看看View接口:
根據(jù)需求可知沼侣,View可以對ID楷兽、FirstName、LastName這三個EditText進行讀操作华临,對FirstName和LastName進行寫操作芯杀,由此定義IUserView接口:
package com.bbno.mvp2.view;
public interface IUserView {
int getID();
String getFirstName();
String getLastName();
void setFirstName(String firstName);
void setLastName(String lastName);
}
(3)Model接口:
同樣,Model也需要對這三個字段進行讀寫操作,并存儲在某個載體內(nèi)(這不是我們所關(guān)心的揭厚,可以存在內(nèi)存却特、文件、數(shù)據(jù)庫或者遠程服務(wù)器筛圆,但對于Presenter及View無影響),定義IUserModel接口:
package com.bbno.mvp2.model;
import com.bbno.mvp2.bean.UserBean;
public interface IUserModel {
void setID(int id);
void setFirstName(String firstName);
void setLastName(String lastName);
UserBean load(int id);//通過id讀取user信息,返回一個UserBean
}
定義類UserModel實現(xiàn)接口IUserModel:
package com.bbno.mvp2.model;
import android.util.Log;
import android.util.SparseArray;
import com.bbno.mvp2.bean.UserBean;
public class UserModel implements IUserModel{
private SparseArray<UserBean> mUserArray = new SparseArray<>();
private int mID;
private String mFirstName;
private String mLastName;
@Override
public void setID(int id) {
mID = id;
}
@Override
public void setFirstName(String firstName) {
mFirstName = firstName;
}
@Override
public void setLastName(String lastName) {
mLastName = lastName;
UserBean mUserBean = new UserBean(mFirstName, mLastName);
mUserArray.append(mID, mUserBean);
}
@Override
public UserBean load(int id) {
mID = id;
UserBean userBean = mUserArray.get(mID, new UserBean("no found", "no found"));
return userBean;
}
}
(4)Presenter:
至此裂明,Presenter就能通過接口與View及Model進行交互了:
package com.bbno.mvp2.presenter;
import com.bbno.mvp2.bean.UserBean;
import com.bbno.mvp2.model.IUserModel;
import com.bbno.mvp2.model.UserModel;
import com.bbno.mvp2.view.IUserView;
public class UserPresenter {
private IUserModel mUserModel;
private IUserView mUserView;
public UserPresenter(IUserView mUserView) {
this.mUserView = mUserView;
this.mUserModel = new UserModel();
}
public void saveUser(int id, String firstName, String lastName){
mUserModel.setID(id);
mUserModel.setFirstName(firstName);
mUserModel.setLastName(lastName);
}
public void loadUser(int id){
UserBean user = mUserModel.load(id);
mUserView.setFirstName(user.getFirstName());
mUserView.setLastName(user.getLastName());
}
}
(5)UserActivity:
UserActivity實現(xiàn)了IUserView接口,同時有一個UserPresenter成員變量:
package com.bbno.mvp2.view;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import com.bbno.basic.R;
import com.bbno.mvp2.presenter.UserPresenter;
import butterknife.Bind;
import butterknife.ButterKnife;
import cn.wwah.basekit.base.activity.BaseActivity;
public class UserActivity extends BaseActivity implements IUserView{
private UserPresenter mUserPresenter = new UserPresenter(this);
@Bind(R.id.edt_id)
EditText edt_id;
@Bind(R.id.edt_first)
EditText edt_first;
@Bind(R.id.edt_last)
EditText edt_last;
@Bind(R.id.btn_save)
Button btn_save;
@Bind(R.id.btn_read)
Button btn_read;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_user);
ButterKnife.bind(this);
initUI();
}
private void initUI() {
btn_save.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mUserPresenter.saveUser(getID(), getFirstName(), getLastName());
}
});
btn_read.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mUserPresenter.loadUser(getID());
}
});
}
@Override
public int getID() {
return Integer.parseInt(edt_id.getText().toString());
}
@Override
public String getFirstName() {
return edt_first.getText().toString();
}
@Override
public String getLastName() {
return edt_last.getText().toString();
}
@Override
public void setFirstName(String firstName) {
edt_first.setText(firstName);
}
@Override
public void setLastName(String lastName) {
edt_last.setText(lastName);
}
}
可以看到太援,View只負責(zé)處理與用戶進行交互闽晦,并把數(shù)據(jù)相關(guān)的邏輯操作都扔給了Presenter去做。而Presenter調(diào)用Model處理完數(shù)據(jù)之后提岔,再通過IUserView更新View顯示的信息仙蛉。
View剩下的方法及UserModel類不是我們所關(guān)心重點。
(6)activity_user.xml布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#fff"
tools:context="com.bbno.mvp2.view.UserActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="80dp"
android:layout_height="wrap_content"
android:gravity="center"
android:text="ID:"
android:textSize="16sp"/>
<EditText
android:id="@+id/edt_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ems="10"
android:textSize="16sp"
android:inputType="textPersonName"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="80dp"
android:layout_height="wrap_content"
android:text="FirstName"
android:textSize="16sp"
android:gravity="center"/>
<EditText
android:id="@+id/edt_first"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ems="10"
android:textSize="16sp"
android:inputType="textPersonName" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/textView2"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:text="LastName"
android:textSize="16sp"
android:gravity="center"/>
<EditText
android:id="@+id/edt_last"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ems="10"
android:textSize="16sp"
android:inputType="textPersonName"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<Button
android:id="@+id/btn_save"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="16sp"
android:text="保存" />
<Button
android:id="@+id/btn_read"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="16sp"
android:text="讀取" />
</LinearLayout>
</LinearLayout>