英文原文:Introduction to Model-View-Presenter on Android
翻譯原文:http://jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0425/2782.html
這是一篇觀點比較激進的文章,完全否定了MVC模式在安卓開發(fā)的中的意義,認為其是沒有任何用處的。這篇文章因為使用了靜態(tài)變量來定義Presenter,因此在原文的評論部分也受到一些爭議。不過我覺得還是從中學到了一些思想。我甚至同意MVC模式在安卓開發(fā)的中毫無意義的說法。沒有完美的文章。另外關于MVP,還看到了一篇思路更清晰的文章,也準備翻譯出來。
這是一篇安卓中MVP模式的詳細教程,從最簡單的例子到最佳實踐。本文還介紹了一個讓在安卓中使用MVP模式變得非常簡單的library。
它是不是很簡單,我們如何才能從中獲益?
什么是MVP
.View是指顯示數(shù)據(jù)并且和用戶交互的層。在安卓中,它們可以是一個Activity,一個Fragment,一個android.view.View或者是一個Dialog。
.Model是數(shù)據(jù)源層。比如數(shù)據(jù)庫接口或者遠程服務器的api。
.Presenter是從Model中獲取數(shù)據(jù)并提供給View的層,Presenter還負責處理后臺任務。
MVP是一個將后臺任務和activities/views/fragment分離的方法,讓它們獨立于絕大多數(shù)跟生命周期相關的事件。這樣應用就會變得更簡單,整個應用的穩(wěn)定性提高10倍以上,代碼也變得更短,可維護性增強,程序員也不會過勞死了~~。
為什么要在安卓上使用MVP
原因之一: 盡量簡單
如果你還沒有閱讀過這篇文章,閱讀它:Kiss原則。- kiss是Keep It Stupid Simple或者Keep It Simple, Stupid的縮寫。
.絕大多數(shù)的安卓程序都只使用了View-Model架構。
.程序員被絞盡了復雜的界面開發(fā)中,而不是解決事務邏輯。
在應用中使用Model-View的壞處是“每個東西之間都是相互關聯(lián)的”如下圖:

如果上面的圖解看起來還不夠復雜,那么想想這些情況:每個view可能在任意的時間出現(xiàn)或者消失,view數(shù)據(jù)需要保存與恢復,在臨時的view上掛載一個后臺任務。
而與“每個東西之間都是相互關聯(lián)的”的相反選擇是使用一個萬能對象(god object)。注:god object是指一個對象/例程在系統(tǒng)中做了太多的事情,或者說是有太多不怎么相關的事情放在一個對象/例程里面來完成。

god object過于復雜,他的不同部分無法重用、測試,無法輕易的debug和重構。
使用MVP

.復雜的任務被分割成簡單的任務。
.更小的對象,更少的bug。
.更好測試
MVP的view層變得如此簡單,在請求數(shù)據(jù)的時候甚至不需要使用回調。view的邏輯變得非常直接。
原因之二: 后臺任務
當你需要寫一個Activity,F(xiàn)ragment或者一個自定義View的時候,你可以將所有和后臺任務相關的方法放在一個外部的或者靜態(tài)的類中。這樣你的后臺任務就不會再與Activity相關聯(lián),不會在泄漏內存同時也不會依賴于Activity的重建。我們稱這樣的一個類為“Presenter”。注:要理解此話的含義最好先看懂第一個MVP示例的代碼。
雖然有一些方法可以解決后臺任務的問題,但是沒有一種和MVP一樣可靠。
為什么這是可行的
下面的圖解顯示了在configuration改變或者發(fā)生out-of-memory事件的情況下應用的不同部分所發(fā)生的事情。每一個開發(fā)者都應該知道這些數(shù)據(jù),但是這些數(shù)據(jù)并不好發(fā)現(xiàn)。
|Case1|Case2|Case3
|A?configuration|Anactivity|A?process
|change|restart|restart
----------------------------------------|-------------|------------|------------
Dialog|reset|reset|reset
Activity,View,Fragment|save/restore|save/restore|save/restore
FragmentwithsetRetainInstance(true)|nochange|save/restore|save/restore
Staticvariablesandthreads|nochange|nochange|reset
情景1:configuration的改變通常發(fā)生在旋轉屏幕,修改語言設置,鏈接外部的模擬器等情況下。要知道更多的configuration change事件請閱讀:configChanges。
情景2:Activity的重啟發(fā)生在當用戶在開發(fā)者選項中選中了“Don't keep activities”(“中文下為 不保留活動”)的復選框,然后另一個Activity在最頂上的時候。
情景3:進程的重啟發(fā)生在應用運行在后臺,但是這個時候內存不夠的情況下。
結論
現(xiàn)在你可以發(fā)現(xiàn),一個擁有setRetainInstance(true)的Fragment并沒有帶來幫助 - 我們還是要保存和/恢復這種fragment的狀態(tài)。因此我們可以去掉可保持Fragment的情景,把問題簡單化。Occam's razor.
|A?configuration|
|change,|
|Anactivity|A?process
|restart|restart
----------------------------------------|-------------|-------------
Activity,View,Fragment,DialogFragment|save/restore|save/restore
Staticvariablesandthreads|nochange|reset
現(xiàn)在看起來就好多了。我們只需要寫兩部分代碼來實現(xiàn)任意情況下完全恢復應用的狀態(tài):
.保存/恢復Activity, View, Fragment, DialogFragment;
.在進程重啟的情況下重新開啟后臺請求。
第一部分我們可以通過常規(guī)的Android API方式來實現(xiàn),第二部分就是Presenter的工作了。Presenter可以記住哪個請求應該被執(zhí)行,并且在執(zhí)行期間如果進程重啟,Presenter可以重新執(zhí)行這些請求。
一個簡單的例子 (未使用MVP)
這個例子將從遠程服務器中加載與顯示一些item元素(就是顯示在ListView中的意思)。如果遇到錯誤會顯示一個toast提示。
我推薦使用RxJava來建立presenter,因為這個庫可以讓數(shù)據(jù)流的控制更簡單。
我還要感謝那個創(chuàng)立了一個簡單api的小伙伴,我的例子中用到了它:The Internet Chuck Norris Database。作者的遠程數(shù)據(jù)就是來自于這個api。貌似是一個提供笑話內容的api。
不使用 MVP示例 00:
publicclassMainActivityextendsActivity{
publicstaticfinalStringDEFAULT_NAME="Chuck?Norris";
privateArrayAdapteradapter;
privateSubscriptionsubscription;
@Override
publicvoidonCreate(BundlesavedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ListViewlistView=(ListView)findViewById(R.id.listView);
listView.setAdapter(adapter=newArrayAdapter<>(this,R.layout.item));
requestItems(DEFAULT_NAME);
}
@Override
protectedvoidonDestroy(){
super.onDestroy();
unsubscribe();
}
publicvoidrequestItems(Stringname){
unsubscribe();
subscription=App.getServerAPI()
.getItems(name.split("\\s+")[0],name.split("\\s+")[1])
.delay(1,TimeUnit.SECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(newAction1(){
@Override
publicvoidcall(ServerAPI.Responseresponse){
onItemsNext(response.items);
}
},newAction1(){
@Override
publicvoidcall(Throwableerror){
onItemsError(error);
}
});
}
publicvoidonItemsNext(ServerAPI.Item[]items){
adapter.clear();
adapter.addAll(items);
}
publicvoidonItemsError(Throwablethrowable){
Toast.makeText(this,throwable.getMessage(),Toast.LENGTH_LONG).show();
}
privatevoidunsubscribe(){
if(subscription!=null){
subscription.unsubscribe();
subscription=null;
}
}
}
注:別被RxJava嚇到,你就當成一般的異步請求就行了。
一個有經(jīng)驗的開發(fā)者應該注意到這個簡單的例子存在很嚴重的問題:
.每次用戶翻轉屏幕的時候都會開始請求 - app做了多余實際需要的請求,并且用戶在旋轉屏幕之后會觀察到一段時間的空白屏幕。
.如果用戶翻轉屏幕的此時很頻繁會導致內存泄漏 - 每次回調都會保存一個對MainActivity的引用,在請求運行的時候這個引用將保存在內存中。這幾乎會必然導致應用因為out-of-memory錯誤或者運行緩慢而崩潰。
譯者注:為什么平時我們沒有發(fā)現(xiàn)這樣的問題?因為我們完全不去考慮用戶頻繁旋轉屏幕的情況,我們認為用戶這樣用手機是找虐,還有,絕大多數(shù)的中文應用都禁止屏幕旋轉,只有豎屏,因此就避免了這種問題的發(fā)生。
使用MVP示例 01:
publicclassMainPresenter{
publicstaticfinalStringDEFAULT_NAME="Chuck?Norris";
privateServerAPI.Item[]items;
privateThrowableerror;
privateMainActivityview;
publicMainPresenter(){
App.getServerAPI()
.getItems(DEFAULT_NAME.split("\\s+")[0],DEFAULT_NAME.split("\\s+")[1])
.delay(1,TimeUnit.SECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(newAction1(){
@Override
publicvoidcall(ServerAPI.Responseresponse){
items=response.items;
publish();
}
},newAction1(){
@Override
publicvoidcall(Throwablethrowable){
error=throwable;
publish();
}
});
}
publicvoidonTakeView(MainActivityview){
this.view=view;
publish();
}
privatevoidpublish(){
if(view!=null){
if(items!=null)
view.onItemsNext(items);
elseif(error!=null)
view.onItemsError(error);
}
}
}
嚴格意義上來說MainPresenter有三個事件:onNext, onError, onTakeView(onNext指代view.onItemsNext,同理onError指代view.onItemsError)。這三個事件在publish()方法中結合到了一起。onNext和onError的值被發(fā)布給了onTakeView()方法提供的MainActivity的實例。
publicclassMainActivityextendsActivity{
privateArrayAdapteradapter;
privatestaticMainPresenterpresenter;
@Override
publicvoidonCreate(BundlesavedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ListViewlistView=(ListView)findViewById(R.id.listView);
listView.setAdapter(adapter=newArrayAdapter<>(this,R.layout.item));
if(presenter==null)
presenter=newMainPresenter();
presenter.onTakeView(this);
}
@Override
protectedvoidonDestroy(){
super.onDestroy();
presenter.onTakeView(null);
if(isFinishing())
presenter=null;
}
publicvoidonItemsNext(ServerAPI.Item[]items){
adapter.clear();
adapter.addAll(items);
}
publicvoidonItemsError(Throwablethrowable){
Toast.makeText(this,throwable.getMessage(),Toast.LENGTH_LONG).show();
}
}
MainActivity創(chuàng)建MainPresenter,并讓它在onCreate/onDestroy的周期之外。MainActivity用靜態(tài)變量來引用MainPresenter,因此每次進程因為out-of-memory事件重啟的時候,MainActivity都會檢查presenter是否還在,如果必要再新建一個。是的,使用靜態(tài)變量看起來會覺得讓人不舒服,但是稍后我們會告訴你如何好看些:
主要的考慮是:
示例程序不會在每次切換屏幕的時候都開始一個新的請求。
如果進程重啟,示例程序會重新加載數(shù)據(jù)。
在MainActivity銷毀(destroyed)的時候MainPresenter不會再持有對MainActivity的引用,因此不會在切換屏幕的時候發(fā)生內存泄漏,而且沒必要去unsubscribe請求。
Nucleus是我從Mortar庫和Keep It Stupid Simple這篇文章得到的靈感而建立的庫。
下面列出其特點:
1.支持在View、Fragment或者Activity的Bundle中保存與恢復Presenter的狀態(tài)。Presenter可以將請求參數(shù)保存在這個bundle中,在稍后重啟請求。
2.只需一行代碼就能將請求的結果與錯誤信息交給view,你不需要寫什么!= null之類的檢查代碼。
3.presenter允許擁有多個View的實例。不過你不能在用Dagger實例化的presenter中這樣使用。
4.支持只用一行代碼將presenter和view綁定。
5.提供一些現(xiàn)成的基類:NucleusView, NucleusFragment, NucleusSupportFragment, NucleusActivity。你可以將他們的代碼拷貝出來改造出一個自己的類以利用Nucleus的presenter。
6.支持在進程重啟的時候自動重啟一個請求,以及在銷毀(onDestroy)期間自動取消RxJava的訂閱。
7.最后,它非常簡單,任何一個開發(fā)者都能理解。只有Presenter的驅動只有180行代碼,而對于RxJava的支持只有230行代碼。
publicclassMainPresenterextendsRxPresenter{
publicstaticfinalStringDEFAULT_NAME="Chuck?Norris";
@Override
protectedvoidonCreate(BundlesavedState){
super.onCreate(savedState);
App.getServerAPI()
.getItems(DEFAULT_NAME.split("\\s+")[0],DEFAULT_NAME.split("\\s+")[1])
.delay(1,TimeUnit.SECONDS)
.observeOn(AndroidSchedulers.mainThread())
.compose(this.deliverLatestCache())
.subscribe(newAction1(){
@Override
publicvoidcall(ServerAPI.Responseresponse){
getView().onItemsNext(response.items);
}
},newAction1(){
@Override
publicvoidcall(Throwablethrowable){
getView().onItemsError(throwable);
}
});
}
}
@RequiresPresenter(MainPresenter.class)
publicclassMainActivityextendsNucleusActivity{
privateArrayAdapteradapter;
@Override
publicvoidonCreate(BundlesavedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ListViewlistView=(ListView)findViewById(R.id.listView);
listView.setAdapter(adapter=newArrayAdapter<>(this,R.layout.item));
}
publicvoidonItemsNext(ServerAPI.Item[]items){
adapter.clear();
adapter.addAll(items);
}
publicvoidonItemsError(Throwablethrowable){
Toast.makeText(this,throwable.getMessage(),Toast.LENGTH_LONG).show();
}
}
就如你看到的那樣,這個例子比前面的例子要簡短多了。Nucleus可以創(chuàng)建/銷毀/保存 presenter,附加或者解除和一個view的關系,并且自動向附加的view發(fā)送請求。
MainPresenter的代碼變短是因為我們使用了deliverLatestCache()操作將數(shù)據(jù)源發(fā)出的所有數(shù)據(jù)與錯誤信息延遲到了view可用之后。它還能將數(shù)據(jù)緩存到內存中,因此可以在onfiguration change的時候重用。
警告!這里有一個注解!在安卓的世界里,如果你使用了注解,最好檢查一下它是否會影響性能。
MainActivity的代碼變簡單了是因為presenter的創(chuàng)建是由NucleusActivity管理的。你只需要寫上@RequiresPresenter(MainPresenter.class) 就能綁定presenter。我在Galaxy S(2010年的設備)上的檢測結果顯示,注解在這里
只花費了不到0.3ms。只在實例化view的時候才會發(fā)生,因此注解在這里對性能的影響可以忽略。
更多示例
帶有保持請求參數(shù)的拓展示例在這里:Nucleus Example.
帶有單元測試的例子:Nucleus Example With Tests。
deliverLatestCache() 方法
這個RxPresenter的工具方法有三個變種:
deliver() will just delay all onNext, onError and onComplete emissions until a View becomes available. Use it for cases when you're doing a one-time request, like logging in to a web service.Javadoc
deliverLatest() will drop the older onNext value if a new onNext value is available. If you have an updatable source of data this will allow you to not accumulate data that is not necessary.Javadoc
deliverLatestCache() is the same as deliverLatest() but in addition it will keep the latest result in memory and will re-deliver it when another instance of a view becomes available (i.e. on configuration change). If you don't want to organize save/restore of a request result in your view (in case if a result is big or it can not be easily saved into Bundle) this method will allow you to make user experience better.Javadoc
Presenter的生命周期
Presenter的生命周期要比安卓組建的生命周期簡短得多
void onCreate(Bundle savedState) - 在Presenter創(chuàng)建的時候調用Javadoc
void onDestroy() - 在用戶離開一個view的時候調用Javadoc
void onSave(Bundle state) - 在View的onSaveInstanceState同時也是Presenter的狀態(tài)保持的時候被調用Javadoc
void onTakeView(ViewType view) -? 在Activity或者Fragment的Resume()或者android.view.View#onAttachedToWindow()的時候調用.Javadoc
void onDropView() - 在Activity或者Fragment的onPause()或者android.view.View#onDetachedFromWindow()的時候調用.Javadoc
View的生命周期與view棧
通常來說你的view(比如fragment或者自定義的view)在用戶的交互過程中掛載與解掛(attached and detached)都是隨機發(fā)生的。 這倒是不讓presenter在view每次解掛(detached)的時候都銷毀的一個啟發(fā)。你可以在任何時候掛載與解掛view,但是presenter可以在這些行為中幸存下來,繼續(xù)后臺的工作。
關于view的周期有一個問題:fragment會因為configuration change或者從棧中去掉而不知道自己是否被解掛(detached)。
Nucleus view默認:只有在activity結束的時候,在view的onDetachedFromWindow()/onDestroy()期間才會銷毀presenter。
因此,如果你要在Activity正常的生命期間銷毀一個view,你必須向view發(fā)出presenter也必須銷毀的信號。通過公共方法NucleusLayout.destroyPresenter()和NucleusFragment.destroyPresenter()來做這個事情。
比如,下面是fragment manager的pop()操作在我的一個項目里是如何工作的:
fragment=fragmentManager.findFragmentById(R.id.fragmentStackContainer);
fragmentManager.popBackStackImmediate();
if(fragmentinstanceofNucleusFragment)
((NucleusFragment)fragment).destroyPresenter();
同樣在fragment的replace操作中也要做相同的事情,在最底部的fragment銷毀的時候也要如此。
你可能會決定在每次view從Activity解掛的時候都銷毀presenter來避免這樣的問題,但是如果這樣的話,在view銷毀的時候你無法繼續(xù)后臺任務。所以這一節(jié)的?"view recycling"完全留你你自己考慮,也許有一天我會找到更好的解決辦法,如果你有一個辦法,請告訴我。
最佳實踐
在Presenter中保存你的請求參數(shù)
規(guī)則很簡單:presenter的主要職能是管理請求。因此view不應該去處理或者開始請求。從view的角度來看,后臺任務是永不消失的,總是會返回一個結果或者錯誤信息的,不需要任何回調的。
publicclassMainPresenterextendsRxPresenter{
privateStringname=DEFAULT_NAME;
@Override
protectedvoidonCreate(BundlesavedState){
super.onCreate(savedState);
if(savedState!=null)
name=savedState.getString(NAME_KEY);
...
@Override
protectedvoidonSave(@NonNullBundlestate){
super.onSave(state);
state.putString(NAME_KEY,name);
}
我推薦你使用酷爆了的Icepick庫。在不使用運行時注解的前提下,它減少了代碼量并且簡化了app的邏輯,所有的事情都在編譯過程中就完成了,是ButterKnife的好伴侶。
publicclassMainPresenterextendsRxPresenter{
@IcicleStringname=DEFAULT_NAME;
@Override
protectedvoidonCreate(BundlesavedState){
super.onCreate(savedState);
Icepick.restoreInstanceState(this,savedState);
...
@Override
protectedvoidonSave(@NonNullBundlestate){
super.onSave(state);
Icepick.saveInstanceState(this,state);
}
如果你有多個請求參數(shù),這個庫可以幫助你節(jié)省不少時間。你可以創(chuàng)建一個BasePresenter,然后將Icepick放到里面,所有的子類將自動保存被@Icicle注釋的變量,你再也不需要實現(xiàn)onSave了。這對于Activity和Fragment或者View也同樣適用。
Execute instant queries on the main thread in onTakeViewJavadoc
有時候我們的數(shù)據(jù)查詢量并不大,比如從數(shù)據(jù)庫中讀取少量的數(shù)據(jù)。雖然使用Nucleus創(chuàng)建一個可重啟的請求非常簡單,但是你不需要每次都用。如果你在fragment創(chuàng)建的時候初始化一個后臺請求,即使只有幾毫秒,用戶也會看到一會兒的空白屏。因此為了代碼的簡潔,也為了用戶的感受,使用主線程來初始化。
不要讓Presenter控制View
這種情況不好工作 - application的邏輯因為使用了不自然的方式變得非常復雜。
最自然的方式是用戶的操作流從view,到presenter到model最后到數(shù)據(jù)。這樣用戶才是控制應用的源頭。對應用的控制應該來源于用戶,而不是應用的內部結構。從view,到presenter到model是很直接的形式,這樣的代碼也很好寫,操作流是這樣的user -> view -> presenter -> model -> data;但是像這樣的操作流:user -> view -> presenter -> view -> presenter -> model -> data,是違背了KISS原則的。
什么?Fragment?不好意思它是違背了這種自然操作流程的。他們太復雜。這里有一篇關于看待fragment的好文章:不提倡 Android Fragment。但是fragment的替代者Flow并沒有簡化多少東西。
MVC
如果你熟悉MVC(Model-View-Controller)- 別那樣做。Model-View-Controller和MVP完全不同,也并沒有解決用戶界面開發(fā)上的任何問題。
什么是MVC?
Modelstands here for internal application state. It can or can not be connected with a storage.
Viewis the only thing that is partially common with MVP - it is a part of an application that rendersModelto the screen.
Controllerrepresents an input device, such as keyboard, mouse or joystick.
MVC在過去以鍵盤為驅動的應用中(比如游戲),是比較好的模式。沒有窗口和圖形用戶界面的交互-程序接受輸入(Controller),維護狀態(tài)(Model),以及顯示輸出(View)。數(shù)據(jù)與操作類似于:controller -> model -> view.但是這種模式在安卓中完全無用。MVC有太多的困擾。人們認為他們在使用MVC,其實使用的的MVP(web開發(fā)者)。許多安卓開發(fā)者認為Controller應該是控制view的東西,因此他們將view的邏輯從view中分離,創(chuàng)建一個輕量級的被代理Controller控制的view。我個人是沒有看出這種架構的好處。
在數(shù)據(jù)復雜的項目中使用固定的數(shù)據(jù)結構
AutoValue是做這件事的一個優(yōu)秀的庫,在他的描述中有其優(yōu)點的列表,建議閱讀。有安卓的接口AutoParcel。使用固定數(shù)據(jù)對象的主要原因是你可以四處傳遞,而不用關心是否在程序的某個地方被修改了。而且它們是線程安全的。
結論
試試mvp吧,并告訴你的朋友。