Android開發(fā)學(xué)習(xí)教程(33)- Android MVP模式簡(jiǎn)單易懂的介紹方式
—— 你要悄悄拔尖,然后一鳴驚人。
Android MVP介紹?MVP 就得先介紹 MVC。在 MVC 模式中,Activity 應(yīng)該是屬于 View 這一層。而實(shí)質(zhì)上,它既承擔(dān)了 View,同時(shí)也包含一些 Controller 的東西在里面。這對(duì)于簡(jiǎn)單的數(shù)據(jù)更新工作會(huì)變得方便許多。)
舉個(gè)簡(jiǎn)單的例子,現(xiàn)在要實(shí)現(xiàn)一個(gè)飄雪的動(dòng)態(tài)壁紙,可以給雪花定義一個(gè)實(shí)體類 Snow,里面存放 XY 軸坐標(biāo)數(shù)據(jù),View 層當(dāng)然就是 SurfaceView(或者其他視圖),為了實(shí)現(xiàn)雪花飄的效果,可以啟動(dòng)一個(gè)后臺(tái)線程,在線程里不斷更新 Snow 實(shí)例里的坐標(biāo)值,這部分就是 Controller 的工作了,Controller 里還要定時(shí)更新 SurfaceView 上面的雪花。進(jìn)一步的話,可以在 SurfaceView 上監(jiān)聽用戶的點(diǎn)擊,如果用戶點(diǎn)擊,只通過 Controller 對(duì)觸摸點(diǎn)周圍的 Snow 的坐標(biāo)值進(jìn)行調(diào)整,從而實(shí)現(xiàn)雪花在用戶點(diǎn)擊后出現(xiàn)彈開等效果。具體的 MVC 模式請(qǐng)自行 Google。
MVP 模式
在 Android 項(xiàng)目中,Activity 和 Fragment 占據(jù)了大部分的開發(fā)工作。如果有一種設(shè)計(jì)模式(或者說代碼結(jié)構(gòu))專門是為優(yōu)化 Activity 和 Fragment 的代碼而產(chǎn)生的,你說這種模式重要不?這就是 MVP 設(shè)計(jì)模式。
按照 MVC 的分層,Activity 和 Fragment(后面只說 Activity)應(yīng)該屬于 View 層,用于展示 UI 界面,以及接收用戶的輸入,此外還要承擔(dān)一些生命周期的工作。Activity 是在 Android 開發(fā)中充當(dāng)非常重要的角色,特別是 TA 的生命周期的功能,所以開發(fā)的時(shí)候我們經(jīng)常把一些業(yè)務(wù)邏輯直接寫在 Activity 里面,這非常直觀方便,代價(jià)就是 Activity 會(huì)越來越臃腫,超過 1000 行代碼是常有的事,而且如果是一些可以通用的業(yè)務(wù)邏輯(比如用戶登錄),寫在具體的 Activity 里就意味著這個(gè)邏輯不能復(fù)用了。如果有進(jìn)行代碼重構(gòu)經(jīng)驗(yàn)的人,看到 1000 + 行的類肯定會(huì)有所顧慮。因此,Activity 不僅承擔(dān)了 View 的角色,還承擔(dān)了一部分的 Controller 角色,這樣一來 V 和 C 就耦合在一起了,雖然這樣寫方便,但是如果業(yè)務(wù)調(diào)整的話,要維護(hù)起來就難了,而且在一個(gè)臃腫的 Activity 類查找業(yè)務(wù)邏輯的代碼也會(huì)非常蛋疼,所以看起來有必要在 Activity 中,把 View 和 Controller 抽離開來,而這就是 MVP 模式的工作了。
MVP 模式的核心思想:
MVP 把 Activity 中的 UI 邏輯抽象成 View 接口,把業(yè)務(wù)邏輯抽象成 Presenter 接口,Model 類還是原來的 Model。
這就是 MVP 模式,現(xiàn)在這樣的話,Activity 的工作的簡(jiǎn)單了,只用來響應(yīng)生命周期,其他工作都丟到 Presenter 中去完成。從上圖可以看出,Presenter 是 Model 和 View 之間的橋梁,為了讓結(jié)構(gòu)變得更加簡(jiǎn)單,View 并不能直接對(duì) Model 進(jìn)行操作,這也是 MVP 與 MVC 最大的不同之處。
MVP 模式的作用
分離了視圖邏輯和業(yè)務(wù)邏輯,降低了耦合
Activity 只處理生命周期的任務(wù),代碼變得更加簡(jiǎn)潔
視圖邏輯和業(yè)務(wù)邏輯分別抽象到了 View 和 Presenter 的接口中去,提高代碼的可閱讀性
Presenter 被抽象成接口,可以有多種具體的實(shí)現(xiàn),所以方便進(jìn)行單元測(cè)試
把業(yè)務(wù)邏輯抽到 Presenter 中去,避免后臺(tái)線程引用著 Activity 導(dǎo)致 Activity 的資源無法被系統(tǒng)回收從而引起內(nèi)存泄露和 OOM
其中最重要的有三點(diǎn):
Activity 代碼變得更加簡(jiǎn)潔
相信很多人閱讀代碼的時(shí)候,都是從 Activity 開始的,對(duì)著一個(gè) 1000 + 行代碼的 Activity,看了都覺得難受。使用 MVP 之后,Activity 就能瘦身許多了,基本上只有 FindView、SetListener 以及 Init 的代碼。其他的就是對(duì) Presenter 的調(diào)用,還有對(duì) View 接口的實(shí)現(xiàn)。這種情形下閱讀代碼就容易多了,而且你只要看 Presenter 的接口,就能明白這個(gè)模塊都有哪些業(yè)務(wù),很快就能定位到具體代碼。Activity 變得容易看懂,容易維護(hù),以后要調(diào)整業(yè)務(wù)、刪減功能也就變得簡(jiǎn)單許多。
避免 Activity 的內(nèi)存泄露
Android APP 發(fā)生 OOM 的最大原因就是出現(xiàn)內(nèi)存泄露造成 APP 的內(nèi)存不夠用,而造成內(nèi)存泄露的兩大原因之一就是 Activity 泄露(Activity Leak)(另一個(gè)原因是 Bitmap 泄露(Bitmap Leak))。
Java 一個(gè)強(qiáng)大的功能就是其虛擬機(jī)的內(nèi)存回收機(jī)制,這個(gè)功能使得 Java 用戶在設(shè)計(jì)代碼的時(shí)候,不用像 C++ 用戶那樣考慮對(duì)象的回收問題。然而,Java 用戶總是喜歡隨便寫一大堆對(duì)象,然后幻想著虛擬機(jī)能幫他們處理好內(nèi)存的回收工作??墒翘摂M機(jī)在回收內(nèi)存的時(shí)候,只會(huì)回收那些沒有被引用的對(duì)象,被引用著的對(duì)象因?yàn)檫€可能會(huì)被調(diào)用,所以不能回收。
Activity 是有生命周期的,用戶隨時(shí)可能切換 Activity,當(dāng) APP 的內(nèi)存不夠用的時(shí)候,系統(tǒng)會(huì)回收處于后臺(tái)的 Activity 的資源以避免 OOM。
采用傳統(tǒng)的 MV 模式,一大堆異步任務(wù)和對(duì) UI 的操作都放在 Activity 里面,比如你可能從網(wǎng)絡(luò)下載一張圖片,在下載成功的回調(diào)里把圖片加載到 Activity 的 ImageView 里面,所以異步任務(wù)保留著對(duì) Activity 的引用。這樣一來,即使 Activity 已經(jīng)被切換到后臺(tái)(onDestroy 已經(jīng)執(zhí)行),這些異步任務(wù)仍然保留著對(duì) Activity 實(shí)例的引用,所以系統(tǒng)就無法回收這個(gè) Activity 實(shí)例了,結(jié)果就是 Activity Leak。
Android 的組件中,Activity 對(duì)象往往是在堆(Java Heap)里占最多內(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。
方便進(jìn)行單元測(cè)試
一般單元測(cè)試都是用來測(cè)試某些新加的業(yè)務(wù)邏輯有沒有問題,如果采用傳統(tǒng)的代碼風(fēng)格(習(xí)慣性上叫做 MV 模式,少了 P),我們可能要先在 Activity 里寫一段測(cè)試代碼,測(cè)試完了再把測(cè)試代碼刪掉換成正式代碼,這時(shí)如果發(fā)現(xiàn)業(yè)務(wù)有問題又得換回測(cè)試代碼,咦,測(cè)試代碼已經(jīng)刪掉了!好吧重新寫吧……MVP 中,由于業(yè)務(wù)邏輯都在 Presenter 里,我們完全可以寫一個(gè) PresenterTest 的實(shí)現(xiàn)類繼承 Presenter 的接口,現(xiàn)在只要在 Activity 里把 Presenter 的創(chuàng)建換成 PresenterTest,就能進(jìn)行單元測(cè)試了,測(cè)試完再換回來即可。萬一發(fā)現(xiàn)還得進(jìn)行測(cè)試,那就再換成 PresenterTest 吧。
MVP 模式的使用
上面一張簡(jiǎn)單的 MVP 模式的 UML 圖,從圖中可以看出,使用 MVP,至少需要經(jīng)歷以下步驟:
1. 創(chuàng)建 IPresenter 接口,把所有業(yè)務(wù)邏輯的接口都放在這里,并創(chuàng)建它的實(shí)現(xiàn) PresenterCompl(在這里可以方便地查看業(yè)務(wù)功能,由于接口可以有多種實(shí)現(xiàn)所以也方便寫單元測(cè)試)
2. 創(chuàng)建 IView 接口,把所有視圖邏輯的接口都放在這里,其實(shí)現(xiàn)類是當(dāng)前的 Activity/Fragment
3. 由 UML 圖可以看出,Activity 里包含了一個(gè) IPresenter,而 PresenterCompl 里又包含了一個(gè) IView 并且依賴了 Model。Activity 里只保留對(duì) IPresenter 的調(diào)用,其它工作全部留到 PresenterCompl 中實(shí)現(xiàn)
4. Model 并不是必須有的,但是一定會(huì)有 View 和 Presenter
通過上面的介紹,MVP 的主要特點(diǎn)就是把 Activity 里的許多邏輯都抽離到 View 和 Presenter 接口中去,并由具體的實(shí)現(xiàn)類來完成。這種寫法多了許多 IView 和 IPresenter 的接口,在某種程度上加大了開發(fā)的工作量,剛開始使用 MVP 的小伙伴可能會(huì)覺得這種寫法比較別扭,而且難以記住。其實(shí)一開始想太多也沒有什么卵用,只要在具體項(xiàng)目中多寫幾次,就能熟悉 MVP 模式的寫法,理解 TA 的意圖,以及享受其帶來的好處。
MVP 模式簡(jiǎn)單實(shí)例
一個(gè)簡(jiǎn)單的登錄界面(實(shí)在想不到別的了╮( ̄▽ ̄”)╭),點(diǎn)擊 LOGIN 則進(jìn)行賬號(hào)密碼驗(yàn)證,點(diǎn)擊 CLEAR 則重置輸入。
項(xiàng)目結(jié)構(gòu)看起來像是這個(gè)樣子的,MVP 的分層還是很清晰的。我的習(xí)慣是先按模塊分 Package,在模塊下面再去創(chuàng)建 model、view、presenter 的子 Package,當(dāng)然也可以用 model、view、presenter 作為頂級(jí)的 Package,然后把所有的模塊的 model、view、presenter 類都到這三個(gè)頂級(jí) Package 中,就好像有人喜歡把項(xiàng)目里所有的 Activity、Fragment、Adapter 都放在一起一樣。
首先來看看 LoginActivity:
public
?class
?LoginActivity?
extends
?ActionBarActivity?
implements
?ILoginView, View.OnClickListener {
????
private
?EditText editUser;
????
private
?EditText editPass;
????
private
?Button?? btnLogin;
????
private
?Button?? btnClear;
????
ILoginPresenter loginPresenter;
????
private
?ProgressBar progressBar;
????
@Override
????
protected
?void
?onCreate(Bundle savedInstanceState) {
????????
super
.onCreate(savedInstanceState);
????????
setContentView(R.layout.activity_main);
????????
//find view
????????
editUser = (EditText)?
this
.findViewById(R.id.et_login_username);
????????
editPass = (EditText)?
this
.findViewById(R.id.et_login_password);
????????
btnLogin = (Button)?
this
.findViewById(R.id.btn_login_login);
????????
btnClear = (Button)?
this
.findViewById(R.id.btn_login_clear);
????????
progressBar = (ProgressBar)?
this
.findViewById(R.id.progress_login);
????????
//set listener
????????
btnLogin.setOnClickListener(
this
);
????????
btnClear.setOnClickListener(
this
);
????????
//init
????????
loginPresenter =?
new
?LoginPresenterCompl(
this
);
????????
loginPresenter.setProgressBarVisiblity(View.INVISIBLE);
????
}
????
@Override
????
public
?void
?onClick(View v) {
????????
switch
?(v.getId()){
????????????
case
?R.id.btn_login_clear:
????????????????
loginPresenter.clear();
????????????????
break
;
????????????
case
?R.id.btn_login_login:
????????????????
loginPresenter.setProgressBarVisiblity(View.VISIBLE);
????????????????
btnLogin.setEnabled(
false
);
????????????????
btnClear.setEnabled(
false
);
????????????????
loginPresenter.doLogin(editUser.getText().toString(), editPass.getText().toString());
????????????????
break
;
????????
}
????
}
????
@Override
????
public
?void
?onClearText() {
????????
editUser.setText(
""
);
????????
editPass.setText(
""
);
????
}
????
@Override
????
public
?void
?onLoginResult(Boolean result,?
int
?code) {
????????
loginPresenter.setProgressBarVisiblity(View.INVISIBLE);
????????
btnLogin.setEnabled(
true
);
????????
btnClear.setEnabled(
true
);
????????
if
?(result){
????????????
Toast.makeText(
this
,
"Login Success"
,Toast.LENGTH_SHORT).show();
????????????
startActivity(
new
?Intent(
this
, HomeActivity.
class
));
????????
}
????????
else
????????????
Toast.makeText(
this
,
"Login Fail, code = "
?+ code,Toast.LENGTH_SHORT).show();
????
}
????
@Override
????
public
?void
?onSetProgressBarVisibility(
int
?visibility) {
????????
progressBar.setVisibility(visibility);
????
}
}
從代碼可以看出 LoginActivity 只做了 findView 以及 setListener 的工作,而且包含了一個(gè) ILoginPresenter,所有業(yè)務(wù)邏輯都是通過調(diào)用 ILoginPresenter 的具體接口來完成。所以 LoginActivity 的代碼看起來很舒爽,甚至有點(diǎn)愉♂悅呢 (/ω\*)。視力不錯(cuò)的你可能還看到了 ILoginView 接口的實(shí)現(xiàn),如果不懂為什么要這樣寫的話,可以先往下看,這里只要記住 “LoginActivity 實(shí)現(xiàn)了 ILoginView 接口”。再來看看 ILoginPresenter
public
?interface
?ILoginPresenter {
????
void
?clear();
????
void
?doLogin(String name, String passwd);
????
void
?setProgressBarVisiblity(
int
?visiblity);
}
public
?class
?LoginPresenterCompl?
implements
?ILoginPresenter {
????
ILoginView iLoginView;
????
IUser user;
????
Handler??? handler;
????
public
?LoginPresenterCompl(ILoginView iLoginView) {
????????
this
.iLoginView = iLoginView;
????????
initUser();
????????
handler =?
new
?Handler(Looper.getMainLooper());
????
}
????
@Override
????
public
?void
?clear() {
????????
iLoginView.onClearText();
????
}
????
@Override
????
public
?void
?doLogin(String name, String passwd) {
????????
Boolean isLoginSuccess =?
true
;
????????
final
?int
?code = user.checkUserValidity(name,passwd);
????????
if
?(code!=
0
) isLoginSuccess =?
false
;
????????
final
?Boolean result = isLoginSuccess;
????????
handler.postDelayed(
new
?Runnable() {
????????????
@Override
????????????
public
?void
?run() {
????????????????
iLoginView.onLoginResult(result, code);
????????????
}
????????
},?
3000
);
????
}
????
@Override
????
public
?void
?setProgressBarVisiblity(
int
?visiblity){
????????
iLoginView.onSetProgressBarVisibility(visiblity);
????
}
????
private
?void
?initUser(){
????????
user =?
new
?UserModel(
"<a href="https://yunjunet.cn/tag/mvp" mvp"target="_blank" style="color: rgb(102, 102, 102); cursor: pointer; transition: all 0.3s ease 0s;">mvp"
,
"mvp"
);
????
}
}
從代碼可以看出,LoginPresenterCompl 保留了 ILoginView 的引用,因此在 LoginPresenterCompl 里就可以直接進(jìn)行 UI 操作了,而不用在 Activity 里完成。這里使用了 ILoginView 引用,而不是直接使用 Activity,這樣一來,如果在別的 Activity 里也需要用到相同的業(yè)務(wù)邏輯,就可以直接復(fù)用 LoginPresenterCompl 類了(一個(gè) Activity 可以包含一個(gè)以上的 Presenter,總之,需要什么業(yè)務(wù)就 new 什么樣的 Presenter,是不是很靈活(@ ̄︶ ̄@)),這也是 MVP 的核心思想:
通過 IVIew 和 IPresenter,把 Activity 的 UI Logic 和 Business Logic 分離開來,Activity just does its basic job! 至于 Model 嘛,還是原來 MVC 里的 Model。
再來看看 ILoginView,至于 ILoginView 的實(shí)現(xiàn)類呢,翻到上面看看 LoginActivity 吧:
public
?interface
?ILoginView {
????
public
?void
?onClearText();
????
public
?void
?onLoginResult(Boolean result,?
int
?code);
????
public
?void
?onSetProgressBarVisibility(
int
?visibility);