寫在前面
最近冷靜了一段時間,複習複習之前學的東西。再加上陰陽師一直抽不到SSR,所以打副本的時候想了想畢設專案架構該怎麼辦。
之前看很多開源軟體實現都是各種 MVP ,看起來很高大上,不過說實話,很早就瞭解 MVP 了,但一直很抗拒去學習,因為覺得模式或者架構類的東西屬於一種思想,並不是固定的寫法,而學習思想之前,必須要學會在引進這種思想之前是如何處理這些問題的。
也就是說,在學 MVP 之前,我得弄明白為啥會提出 MVP ,因為在 MVP 提出之前都是用 MVC 去處理的,所以我得學會 MVC ,當我能熟練的使用 MVC 的時候,再去學習 MVP ,這樣能很清楚的明白兩者之間的區別或者各自的優缺點,個人覺得這樣學起來還是比較好的,而不是盲目跟風,包括現在很多部落格提到的 React Native 、 Dagger2 等等都是一樣的道理。
現在我也來簡單聊一聊我自己所理解的 MVP ,不過只能算個入門吧。
不可少的介紹
關於 MVP 大家或多或少都知道一點,網上關於 MVP 的教程也很多,不過優質的就太少了。入門我只看了兩篇文章(泡網+鴻洋),文末都會有連結。
先上一個經典的圖:
C 和 P 的區別
先來看一下 MVP 與 MVC 差別在哪?簡單一眼掃過,就是 C 和 P 的差別。
1、先看 C
C 就是 Controller,控制器。負責從 View 讀取資料,控制使用者輸入,並向 Model 傳送資料。簡單來說,就是起到一個溝通的作用,能很大程度上的解決 Model 和 View 的耦合問題。
換句話說就是,它是一個 Model 與 View 之間的橋樑,讓 Model 和 View 之間不再緊緊關聯。
比如 View 接收到了使用者輸入資料,先交給 Controller ,Controller 再轉交給 Model ,反之亦然。
這就像小明喜歡隔壁班小紅,小明寫了一封情書需要通過隔壁班小王,才能交給小紅。
但是注意,我只是說能很大程度上解決,並不能徹底解決,也就是說小明如果發現了隔壁小王有問題,他仍然可以選擇直接把情書交給小紅。
2、再看 P
P 就是 Presenter,我翻譯成主持者。跟 C 類似,仍然是負責 View 和 Model 之間的溝通。但是它徹底讓 View 和 Model 不能直接溝通。如果想要溝通,就必須通過這個主持者來主持它們兩個應該幹啥。
比如 View 接收到了使用者輸入資料,不能直接給 Model ,要交給 Presenter ,Presenter 再轉交給 Model ,反之亦然。
這就像我給主席寄了一個包裹,但這個包裹必須經過重重安檢,才能交到主席手上。
這就徹底斷了我跟主席……哦不對,Model 和 View 之間的聯絡。
3、簡單區別
僅從目前來看, C 和 P 都是為了解放 Model 和 View 之間的聯絡,只不過 C 是很大程度上解決,但 P 是徹底讓它們兩斷了聯絡。
換成技術術語來說就是一句話:
C 讓 Model 和 View 做到 鬆散耦合,而 P 直接將它們 解耦。
MVC 和 MVP 的區別
知道了各自簡單的作用,再來更深層次的理解 C 和 P 在各自的 MV+X 中到底分別做了什麼?
1、先看 MVC
從下圖中我們可以看到:
- 使用者 Event(事件)會導致 Controller 改變 Model 或 View 或同時改變兩者。
- 只要 Controller 改變了 Model 的資料或屬性,所有依賴的 View 都會自動更新。
- 類似的,只要 Controller 改變了 View ,View 會從潛在的 Model 中獲取資料進行更新。
2、再看 MVP
從下圖中我們又能看到:
- Presenter 中同時持有 View 以及 Model 的 Interface 引用,而 View 持有 Presenter 的例項。
- 當某個 View 需要展示某些資料時,首先會呼叫 Presenter 的某個介面,然後 Presenter 會呼叫 Model 請求資料。
- 當 Model 資料載入成功後會呼叫 Presenter 的回撥方法通知 Presenter 資料載入完畢,最後 Presenter 再呼叫 View 層介面展示載入後資料。
3、主要區別
在 MVC 中:
- View 可以與 Model 直接互動;
- Controller 可以被多個 View 共享;
- Controller 可以決定顯示哪個 View。
在 MVP 中:
- View 不直接與 Model 互動;
- Presenter 與 View 通過介面來互動,更有利於新增單元測試;
- 通常 View 與 Presenter 是一對一的,但複雜的 View 可能繫結多個 Presenter 來處理;
- Presenter 也可以直接進行 View 上的渲染。
經典案例
當然是那個經典的登入案例,不過這裡順帶學下畢設裡幾個 MD 風格的開源庫。先來看一下執行的效果圖吧:
先分析
好了,動手之前先分析一下。
從上面內容我們知道,Presenter 是用來 Model 和 View 之間互動的。所以必須要持有它們各自的物件,根據需求一般都是用介面來實現。
而實現 View 層介面的一般都是 Activity (暫且這樣認為,後文還需要討論)。
當然如果想要 Activity 和 Model 進行互動,那麼這個 Activity 中還必須有一個 Presenter 的例項,因為需要這個 Presenter 來進行互動嘛!
OK,把上面所有的東西捋一捋,數一數到底需要啥:
- Model:負責儲存、檢索、操縱資料,一般都會一些封裝對 Bean 進行操作。
- ModelInterface:這個不是必須的,但有時候如果幾個 Bean 之間有共性,可以抽一個介面出來。
- View:暫且就認為是 Activity 。
- ViewInterface:View 需要實現的介面,View 和 Presenter 也是通過它來進行互動。
- Presenter:最重要的 View 和 Model 的橋樑,處理與使用者互動的負責邏輯,需要持有 View 和 Model 的介面物件。
雖然看起來東西確實變多了,但是結構看起來還是很清晰的,擴充套件起來也比較方便。
再動手
按照上面需要的東西,一步一步來:
1、先建一個 Bean
/**
* @author xiarui 16/09/20
* @description Person的Bean類
*/
public class PersonBean {
private String name ;
private String pwd;
//...省略
}
複製程式碼
2、再建立 Model Interface
針對這個 Bean ,有註冊和登入的功能,這裡強行抽取一個 IPersonModel 介面出來,純屬為了展示用,意義不大:
/**
* @author xiarui 16/09/20
* @description IPersonModel介面
* @remark 介面其實不必實現 只是為了講解例子強行抽取的方法
*/
public interface IPersonModel {
//註冊賬號
boolean onRegister(String name, String pwd);
//登入賬號
boolean onLogin(String name, String pwd);
}
複製程式碼
3、其次建立 Model
實現了上一步建立的 Model Interface ,主要是對註冊和登入方法的實現:
**
* @author xiarui 16/09/20
* @description Model類 實現IPersonModel介面
* @remark 介面其實不必實現 只是為了講解例子強行實現的
*/
public class PersonModel implements IPersonModel {
//簡單的存一下注冊的賬號
private Map<String, String> personMap = new HashMap<>();
/**
* 註冊賬號 存入集合
*
* @param name 使用者名稱
* @param pwd 密碼
* @return true:註冊成功,false:註冊失敗
*/
@Override
public boolean onRegister(String name, String pwd) {
if (!personMap.containsKey(name)) {
personMap.put(name, pwd);
return true;
}
return false;
}
/**
* 登入賬號
*
* @param name 使用者名稱
* @param pwd 密碼
* @return true:登入成功,false:登入失敗
*/
@Override
public boolean onLogin(String name, String pwd) {
return pwd.equals(personMap.get(name));
}
}
複製程式碼
4、還需要 View Interface
在這裡我設定了五個方法,其中註冊/登入成功與否分別建了兩個方法,原因後文再說:
/**
* @author xiarui 16/09/20
* @description IPersonView介面
*/
public interface IPersonView {
boolean checkInputInfo(); //檢查輸入的合法性
void onRegisterSucceed(); //註冊成功
void onRegisterFaild(); //註冊失敗
void onLoginSucceed(); //登入成功
void onLoginFaild(); //登入失敗
}
複製程式碼
5、最重要的 Presenter
再次強調,Presenter 是用來 Model 和 View 互動的,而它們各自都實現了介面,那我們只需保證 Presenter 持有這些介面即可:
/**
* @author xiarui 16/09/20
* @description Person的Presenter類
* @remark 必須要傳M和V 因為P需要控制M和V
*/
public class PersonPresenter {
private IPersonModel mPersonModel; //Model介面
private IPersonView mPersonView; //View介面
public PersonPresenter(IPersonView mPersonView) {
mPersonModel = new PersonModel();
this.mPersonView = mPersonView;
}
public void registerPerson(String name, String pwd) {
boolean isRegister = mPersonModel.onRegister(name, pwd);
//根據Model中的結果呼叫不同的方法進行UI展示
if(isRegister){
mPersonView.onRegisterSucceed();
}else{
mPersonView.onRegisterFaild();
}
}
public void loginPerson(String name, String pwd) {
boolean isLogin = mPersonModel.onLogin(name, pwd);
//根據Model中的結果呼叫不同的方法進行UI展示
if (isLogin) {
mPersonView.onLoginSucceed();
}else{
mPersonView.onLoginFaild();
}
}
}
複製程式碼
6、最後的 View
這裡的 View 其實就是實現 IPersonView 介面的 Activity,它必須有一個 Presenter 的例項才能與 Model 互動:
原始碼有刪減,保留核心方法
/**
* @author xiarui 16/09/20
* @description MVP的簡單例子
* @remark View 必須持有 Presenter 的例項才能與 Model 互動
*/
public class MainActivity extends AppCompatActivity implements IPersonView, View.OnClickListener {
/*===== 資料相關 =====*/
private PersonPresenter personPersenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView(); //初始化View
initData(); //初始化Data
}
/**
* 初始化Data
*/
private void initData() {
personPersenter = new PersonPresenter(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.bt_main_register:
if (checkInputInfo()) {
personPersenter.registerPerson(inputName, inputPwd);
}
break;
case R.id.bt_main_login:
if (checkInputInfo()) {
personPersenter.loginPerson(inputName, inputPwd);
}
break;
}
}
/*========== IPersonView介面方法 START ==========*/
/**
* 檢查輸入資訊的合法性
*
* @return true:輸入合法,false:輸入不合法
*/
@Override
public boolean checkInputInfo() {
inputName = nameEText.getText().toString().trim();
inputPwd = pwdEText.getText().toString().trim();
if (inputName.equals("")) {
nameEText.setError("使用者名稱不能為空");
return false;
}
if (inputPwd.equals("")) {
pwdEText.setError("密碼不能為空");
return false;
}
return true;
}
@Override
public void onRegisterSucceed() {
showToast("註冊成功");
}
@Override
public void onRegisterFaild() {
showToast("使用者已存在");
}
@Override
public void onLoginSucceed() {
showToast("登入成功");
}
@Override
public void onLoginFaild() {
showToast("使用者不存在或密碼錯誤");
}
/*========== IPersonView介面方法 END ==========*/
}
複製程式碼
當完成這些步驟後,一個簡單的 MVP 示例就完成了。
Q & A
這裡是一些疑問和解答:
Q: MVP 模式中 View 層是否就是 Activity ?
A: 其實嚴格意義上來說,這麼說是不對的。雖然本例中確實是 Activity ,但是在真正的專案中,需要考慮 Activity 和 Fragment 的情況,甚至還要考慮一些特定的 View 或者 ViewGroup 。
注:後面我就用 Activity 統一指代 View 了。
Q: 從例子上看,幾乎每一個 Activity 都對應著 一個 Presenter ,還需要其他的介面,那如果 Activity 很多怎麼辦?
A: 其實這個問題一直是 MVP 飽受詬病的地方,雖然 MVP 結構很清晰,但確實要增加很多很多的類,所以需要儘量讓介面能適用於多種 View ,但如果實在忍受不了,建議不用 MVP。
Q: 使用 MVP 後感覺專案更加臃腫和複雜了怎麼辦?
A: 從來都沒有人說過 MVP 能使得專案簡單,只是它會讓專案結構更加清晰更加易於擴充套件而已。就像 RxJava 一樣,程式碼量還是那麼多,但是流程更加清晰了,這就是能讓開發者擁護的原因。
Q: 為什麼案例中 IPersonView 這個介面將註冊登入成功與否分開成獨立方法?
A: 這裡確實可以不分開,只要將註冊/登入的結果作為引數即可,但是這樣的話,我們仍然需要在 Activity 中根據結果引數來決定顯示的 Toast 內容。
也就是說 View 仍然需要處理一些來自 Model 的邏輯,這樣不是太符合 MVP 的意義。所以將判斷邏輯放在 Presenter 中處理,View 層只管展示就行了。
包括鴻洋大神的那篇文章中,有一個 View 的方法直接傳遞了涉及 Model 層的類,顯然違背了 MVP 的定義,我覺得不是太好(批判了大神,果斷逃……)。
Q: Presenter 如果進行耗時操作,但此時對應的 Activity 被殺死,會報空指標麼?
A: 其實在這種情況下,已經存在記憶體洩漏的情況了。但有意思的是,並不會報空指標,具體原因暫時還不是特別清楚,但好友xiasuhuei321提醒我說,可能回收的時候並沒有完全回收,因為系統會認為還存在相關的引用,所以不會空指標。
Q: 那該如何避免記憶體洩漏這種情況呢?
A: 這個問題我看的時候覺得很簡單,後來發現這是很有趣的問題。具體方法有很多,也有很多的開源庫專門處理這樣的問題。其實解決辦法歸納起來就是一個 如何讓 Presenter 的生命週期跟 Activity 的生命週期保持一致。
我看了很多方法,只覺得通過 Loader 的方法來解決是最簡單也最有效的方法。但是我還沒有徹底學完,暫時不班門弄斧,有興趣可以直接點選下面的連結進行學習:
總結
到此,關於 MVP 的簡單入門級知識大概就說完了,雖然網上教程很多很多,但還是用自己的話去講清楚比較舒服。當然了, MVP 可遠遠不止這些,其他的東西學到之後再提吧。
不過就像開頭說的那樣,這東西就是一個思想,沒必要死板硬套,再者說了谷歌不是又推出了 MVVM 了麼。說到 MVVM 又頭疼,感覺總有學不完的東西,雖然總比別人慢一步,但是沒辦法,學技術得冷靜。
當別人大張旗鼓的時候,更要謀自己的路,證自己的道。
參考資料
下面兩篇是我的入門教程,寫的不錯:
下面這個確實對得起標題,真的很詳細,主要是一些資源綜合,有上下兩篇,這裡只貼上篇,都很有價值:
下面這個是我朋友寫的,也很詳細而清晰,例子也很具有代表性:
哦對了,這是 MD 風格控制元件的開源庫,扔物線大神的:
專案原始碼
個人部落格:www.iamxiarui.com