Model-View-Presenter
本文主要介紹 Model-View-Presenter (MVP)的原理凰慈,以及如何使用Mosby創(chuàng)建基于MVP的應(yīng)用程序波闹。
- model 是將在視圖(用戶界面)中顯示的數(shù)據(jù)尾序。
- view 是顯示數(shù)據(jù)(model)并將用戶命令(事件)傳遞到 Presenter 以對(duì)該數(shù)據(jù)執(zhí)行操作的界面惶翻。view 通常對(duì)其Presenter有一個(gè)引用。
- Presenter 是“中間人”(就像MVC中的controller),并具有view和model的引用。請(qǐng)注意,“Model”一詞是誤導(dǎo)性的筋量。它應(yīng)該是檢索或操縱模型的業(yè)務(wù)邏輯。例如:如果你有一個(gè)在數(shù)據(jù)庫(kù)表中存儲(chǔ)User的數(shù)據(jù)庫(kù)碉熄,并且你的View想要顯示一個(gè)User列表桨武,那么Presenter會(huì)引用數(shù)據(jù)庫(kù)中的業(yè)務(wù)邏輯層(比如DAO)從而查詢到一個(gè)User列表。
[圖片上傳失敗...(image-ff1b01-1513151286080)]
查詢和顯示來自數(shù)據(jù)庫(kù)的用戶列表的具體工作流程:
[圖片上傳失敗...(image-190b62-1513151241674)]
上面顯示的工作流程圖應(yīng)該是很容易理解的锈津。不過這里有一些額外的想法:
Presenter
并不是OnClickListener
呀酸。View
負(fù)責(zé)處理用戶輸入并調(diào)用Presenter
的相應(yīng)方法。為什么不通過將Presenter
變成OnClickListener
從而消除這種"轉(zhuǎn)移"的過程呢琼梆?如果這樣做性誉,Presenter需要了解有關(guān)視圖內(nèi)部的知識(shí)。例如茎杂,如果一個(gè)View有兩個(gè)按鈕错览,并且這個(gè)view在這兩個(gè)按鈕上都把Presenter
注冊(cè)成OnClickListener
,那么Presenter
如何區(qū)分哪個(gè)按鈕被點(diǎn)擊了(在不知道view按鈕引用等內(nèi)部構(gòu)造的情況下)煌往? Model,View和Presenter應(yīng)該分離刽脖。而且羞海,如果讓Presenter
實(shí)現(xiàn)OnClickListener
,Presenter就被綁定到了android平臺(tái)曲管。從理論上說却邓,Presenter和業(yè)務(wù)邏輯應(yīng)該能夠在桌面程序或其他java程序間共享的普通java代碼。就像在步驟1和步驟2中看到的翘地,
View
只做Presenter
告訴View
需要做的那些操作:用戶點(diǎn)擊了“l(fā)oad user button”(第1步)之后,view不會(huì)直接顯示加載動(dòng)畫癌幕。而是在步驟2由Presenter明確地告訴view去顯示加載動(dòng)畫衙耕。Model-View-Presenter的這種變體被稱為被動(dòng)視圖(Passive View)。view應(yīng)該盡可能愚蠢勺远。讓Presenter以抽象的方式控制view橙喘。例如:Presenter調(diào)用view.showLoading()
,而不是控制view中特定的東西,如動(dòng)畫胶逢。所以厅瞎,Presenter不應(yīng)該調(diào)用view.startAnimation()
這種方法饰潜。通過實(shí)現(xiàn)MVP被動(dòng)視圖,處理并發(fā)和多線程更容易和簸。就像您在步驟3中看到的那樣彭雾,數(shù)據(jù)庫(kù)查詢異步運(yùn)行,Presenter是一個(gè)監(jiān)聽器Listener/觀察者Observer锁保,并在數(shù)據(jù)準(zhǔn)備好顯示時(shí)得到通知薯酝。
Android上的MVP
到現(xiàn)在為止還挺好。但是如何在自己的Android應(yīng)用上應(yīng)用MVP爽柒?第一個(gè)問題是吴菠,我們應(yīng)該在哪里應(yīng)用MVP模式?在Activity上浩村,F(xiàn)ragment上做葵,還是在像RelativeLayout這樣的ViewGroup上?讓我們來看看Gmail Android平板應(yīng)用程序:
[圖片上傳失敗...(image-c3a04-1513151241674)]
在我們看來心墅,在上圖所示的屏幕上有四個(gè)獨(dú)立的可使用MVP的地方酿矢。“可以使用MVP的地方”是指屏幕上顯示的嗓化、在邏輯上屬于一個(gè)整體的UI元素棠涮。因此這些地方也可以稱為是可以運(yùn)用MVP的一個(gè)單獨(dú)的UI單元。
[圖片上傳失敗...(image-b543c2-1513151241674)]
這樣看起來MVP似乎適合運(yùn)用到Activity,特別是Fragment上刺覆。通常一個(gè)Fragment負(fù)責(zé)顯示一個(gè)像ListView一樣的內(nèi)容严肪。例如上圖中被使用MailProvider
獲取Mails
列表的InboxPresenter
控制的InboxView
。但是谦屑,MVP不限于Fragment 和 Activity驳糯。你也可以在ViewGroups
上應(yīng)用這個(gè)設(shè)計(jì)模式,如上圖所示的SearchView
氢橙。在許多app中都在Fragment上使用MVP酝枢。然而,這都取決于你想要把MVP運(yùn)用到什么地方悍手。只要確保view是獨(dú)立的帘睦,以便一個(gè)Presenter可以控制這個(gè)view,而不會(huì)與另一個(gè)Presenter發(fā)生沖突坦康。
我們?yōu)槭裁匆獙?shí)現(xiàn)MVP竣付?
思考一下,如果不使用MVP滞欠,你將如何在Fragment中實(shí)現(xiàn)收件箱view古胆,來顯示從本地sql數(shù)據(jù)庫(kù)和IMAP郵件服務(wù)器兩個(gè)數(shù)據(jù)源得到的郵件列表。你的Fragment代碼會(huì)是什么樣子筛璧?或許逸绎,你將啟動(dòng)兩個(gè)AsyncTasks
并且必須實(shí)現(xiàn)一個(gè)“等待機(jī)制”(等到兩個(gè)任務(wù)都完成)惹恃,然后將兩個(gè)任務(wù)得到的郵件列表合并成一個(gè)郵件列表。你還需要注意棺牧,在加載時(shí)顯示加載動(dòng)畫(ProgressBar)巫糙,之后用ListView替換它。你會(huì)把所有的代碼放入Fragment嗎陨帆?如果加載時(shí)出現(xiàn)了錯(cuò)誤怎么辦曲秉?如果屏幕方向改變了呢?誰(shuí)負(fù)責(zé)取消AsyncTasks
疲牵?這一系列的問題都可以用MVP來解決承二。讓我們向1000+行、大雜燴似的Activity和Fragment代碼說再見吧纲爸。
但是在我們深入了解如何在Android上實(shí)現(xiàn)MVP之前亥鸠,我們必須澄清一下,Activity和Fragment到底是View
還是Presenter
识啦。Activity和Fragment似乎既是View又是Presenter负蚊,因?yàn)樗麄兌加?code>onCreate()和onDestroy()
這種生命周期回調(diào),也有像從一個(gè)UI控件切換到另一個(gè)UI控件(例如颓哮,加載時(shí)顯示一個(gè)ProgressBar家妆,然后顯示一個(gè)帶有數(shù)據(jù)的ListView)的View職責(zé)。你可以說這些聽起來Activity和Fragment更像是一個(gè)Controller冕茅。然而伤极,我們得出的結(jié)論是Activity和Fragment應(yīng)該被視為(愚蠢的)View,而不是Presenter姨伤。后面你會(huì)看到原因哨坪。
有了這個(gè)說法,我們想要介紹Mosby
乍楚,這是一個(gè)在android上創(chuàng)建基于MVP的應(yīng)用程序的庫(kù)当编。
Mosby
你可能已經(jīng)發(fā)現(xiàn),如果你試圖去解釋MVP是MVC(Model-View-Controller)的變種或改進(jìn)徒溪,那么就很難理解什么是Presenter忿偷。尤其是iOS開發(fā)人員,他們很難理解Controller和Presenter的區(qū)別臊泌, because they “grew up” with the fixed idea and definition of an iOS alike UIViewController
鲤桥。在我們來看,MVP并不是MVC的變種或改進(jìn)缺虐,因?yàn)檫@意味著Presenter取代了Controller芜壁。我們認(rèn)為礁凡,MVP包裝了MVC高氮』弁看看你使用MVC開發(fā)的app。通常你有你的View和Controller(即Android中的Fragment或iOS的UIViewController)處理點(diǎn)擊事件剪芍,綁定數(shù)據(jù)和觀察ListView(或在iOS上為UITableView實(shí)現(xiàn)一個(gè)UITableViewDelegate)等等∪停現(xiàn)在退一步,想象一下罪裹,controller就是view的一部分饱普,而不是直接連接到你的model(業(yè)務(wù)邏輯)。而Presenter位于controller 和model的中間状共,如下所示:
[圖片上傳失敗...(image-821785-1513151241674)]
讓我們來看一個(gè)具體的例子:示例程序顯示從數(shù)據(jù)庫(kù)中查詢的用戶列表套耕。當(dāng)用戶點(diǎn)擊“加載按鈕”時(shí)開始執(zhí)行。查詢數(shù)據(jù)庫(kù)(異步)時(shí)ProgressBar顯示峡继,然后 ListView顯示出查詢結(jié)果冯袍。
我們認(rèn)為Presenter不會(huì)取代Controller。而是Presenter協(xié)調(diào)并監(jiān)督Presenter所屬的View碾牌。Controller是處理點(diǎn)擊事件并調(diào)用相應(yīng)的Presenter方法的組件康愤。Controller是負(fù)責(zé)控制動(dòng)畫的組件,如隱藏ProgressBar并顯示ListView舶吗。Controller監(jiān)聽ListView上的滾動(dòng)事件征冷,即在滾動(dòng)ListView時(shí)進(jìn)行一些item動(dòng)畫或顯示隱藏toolbar。因此誓琼,所有與UI相關(guān)的東西仍然受Controller而不是Presenter控制(即Presenter不應(yīng)該是一個(gè)OnClickListener)检激。Presenter負(fù)責(zé)協(xié)調(diào)view層(由UI控件和Controller組成)的整體狀態(tài)。因此踊赠,Presenter的工作是告訴view層現(xiàn)在應(yīng)該顯示加載動(dòng)畫呵扛,然后在數(shù)據(jù)準(zhǔn)備好后,顯示ListView筐带。
MvpView和MvpPresenter
所有view的基類是MvpView
今穿。本質(zhì)上它只是一個(gè)空的interface。該接口為Presenter提供了一個(gè)公共API來調(diào)用View相關(guān)的方法伦籍。Presenter的基類是MvpPresenter
:
public interface MvpView { }
public interface MvpPresenter<V extends MvpView> {
public void attachView(V view);
public void detachView(boolean retainInstance);
}
這一理念是MvpView
(即Fragment or Activity)會(huì)去關(guān)聯(lián)和取消關(guān)聯(lián)一個(gè)MvpPresenter
蓝晒。這樣一來Mosby獲取到Activity和Fragment的生命周期(更多內(nèi)容可以查看下面“委托”部分的內(nèi)容)。因此帖鸦,初始化和清理東西(如取消異步運(yùn)行任務(wù))的操作應(yīng)該在presenter.attachView()
和presenter.detachView()
中執(zhí)行芝薇。
Mosby提供了Presenter的另一種實(shí)現(xiàn)MvpBasePresenter
,它使用WeakReference
來保存對(duì)view(Fragment or Activity)的引用作儿,以避免內(nèi)存泄漏洛二。因此,當(dāng)你的Presenter想要調(diào)用view的方法時(shí),您必須通過調(diào)用isViewAttached()
來檢查這個(gè)view是否被關(guān)聯(lián)到你的Presenter晾嘶,并通過使用
getView()
來或者view的引用妓雾。
另外,你可以為你的MvpView
使用實(shí)現(xiàn)了空對(duì)象模式的MvpNullObjectBasePresenter
垒迂。所以無論什么時(shí)候MvpNullObjectBasePresenter.onDetach()
被調(diào)用械姻,view都不會(huì)被設(shè)置為null(像MvpBasePresenter這樣),而是通過使用反射來動(dòng)態(tài)創(chuàng)建一個(gè)空view机断,并將其作為view關(guān)聯(lián)到Presenter中楷拳。這就避免了在方法調(diào)用時(shí)檢查view != null
。
MvpActivity和MvpFragment
如前所述吏奸,我們將Activity 和 Fragment當(dāng)作View欢揖。如果你只是想要一個(gè)由Presenter控制的Activity 或 Fragment,你可以在你的程序中使用實(shí)現(xiàn)了MvpView
的MvpActivity
和MvpFragment
用作基類奋蔚。為了確保類型安全浸颓,建議這樣使用:MvpActivity<V extends MvpView, P extends MvpPresenter>
和MvpFragment<V extends MvpView, P extends MvpPresenter>
加載內(nèi)容錯(cuò)誤(LCE)
通常你會(huì)發(fā)現(xiàn)自己在應(yīng)用程序中一遍又一遍地寫同樣的東西:在后臺(tái)加載數(shù)據(jù),在加載時(shí)顯示加載視圖(即ProgressBar)旺拉,顯示加載的數(shù)據(jù)或加載錯(cuò)誤時(shí)顯示錯(cuò)誤消息产上。由于SwipeRefreshLayout
成為Android的支持庫(kù)的一部分,現(xiàn)在支持下拉刷新是很容易的蛾狗。為了不重復(fù)實(shí)施這個(gè)工作流程Mosby提供了MvpLceView
:
/**
* @param <M> The type of the data displayed in this view
*/
public interface MvpLceView<M> extends MvpView {
/**
* Display a loading view while loading data in background.
* <b>The loading view must have the id = R.id.loadingView</b>
*
* @param pullToRefresh true, if pull-to-refresh has been invoked loading.
*/
public void showLoading(boolean pullToRefresh);
/**
* Show the content view.
*
* <b>The content view must have the id = R.id.contentView</b>
*/
public void showContent();
/**
* Show the error view.
* <b>The error view must be a TextView with the id = R.id.errorView</b>
*
* @param e The Throwable that has caused this error
* @param pullToRefresh true, if the exception was thrown during pull-to-refresh, otherwise
* false.
*/
public void showError(Throwable e, boolean pullToRefresh);
/**
* The data that should be displayed with {@link #showContent()}
*/
public void setData(M data);
}
上面說的那種view晋涣,你可以使用MvpLceActivity implements MvpLceView
和MvpLceFragment implements MvpLceView
來實(shí)現(xiàn)。這兩個(gè)都假設(shè)XML布局中包含了含有R.id.loadingView
沉桌,R.id.contentView
和R.id.errorView
的view谢鹊。
示例
在下面的示例中(托管在Github上),我們通過使用CountriesAsyncLoader
加載Country
列表并在Fragment的RecyclerView中顯示留凭。
我們首先定義視圖界面CountriesView
:
public interface CountriesView extends MvpLceView<List<Country>> {
}
為什么我需要為View定義接口佃扼?
由于它是一個(gè)接口,你可以改變view的實(shí)現(xiàn)蔼夜。我們可以簡(jiǎn)單的將代碼從繼承Activity的實(shí)現(xiàn)中拷貝到繼承Fragment的實(shí)現(xiàn)中兼耀。
模塊化:您可以將整個(gè)業(yè)務(wù)邏輯,Presenter和View Interface移動(dòng)到獨(dú)立的庫(kù)中求冷。然后瘤运,把這個(gè)包含了Presenter的庫(kù)應(yīng)用到各種app中。
您可以輕松編寫單元測(cè)試匠题,因?yàn)槟梢酝ㄟ^實(shí)現(xiàn)view interface來模擬視圖拯坟。還有一個(gè)更簡(jiǎn)單的方法就是在presenter中引入java接口并模擬presenter對(duì)象來編寫單元測(cè)試。
為視圖定義一個(gè)接口的另一個(gè)好處是韭山,你不需要直接從Presenter中調(diào)用activity / fragment的方法郁季。因?yàn)樵趯?shí)現(xiàn)Presenter的時(shí)候冷溃,你在IDE的自動(dòng)完成提示中只能看到view interface的那些方法。根據(jù)我們的個(gè)人經(jīng)驗(yàn)梦裂,我們可以說秃诵,這是非常有用的,特別是如果你在一個(gè)團(tuán)隊(duì)中工作塞琼。
請(qǐng)注意,我們也可以使用MvpLceView<List<Country>>
禁舷,而不是定義一個(gè)(空的彪杉,因?yàn)槔^承方法)接口CountriesView
。但是有一個(gè)專用的接口CountriesView可以提高代碼的可讀性牵咙,而且我們可以在將來更靈活地定義更多的與View有關(guān)的方法派近。
接下來我們用所需的id來定義我們view的xml布局文件:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<!-- Loading View -->
<ProgressBar
android:id="@+id/loadingView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
/>
<!-- Content View -->
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/contentView"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</android.support.v4.widget.SwipeRefreshLayout>
<!-- Error view -->
<TextView
android:id="@+id/errorView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
</FrameLayout>
CountriesPresenter
控制CountriesView
并啟動(dòng)CountriesAsyncLoader
:
public class CountriesPresenter extends MvpBasePresenter<CountriesView> {
@Override
public void loadCountries(final boolean pullToRefresh) {
getView().showLoading(pullToRefresh);
CountriesAsyncLoader countriesLoader = new CountriesAsyncLoader(
new CountriesAsyncLoader.CountriesLoaderListener() {
@Override public void onSuccess(List<Country> countries) {
if (isViewAttached()) {
getView().setData(countries);
getView().showContent();
}
}
@Override public void onError(Exception e) {
if (isViewAttached()) {
getView().showError(e, pullToRefresh);
}
}
});
countriesLoader.execute();
}
}
實(shí)現(xiàn)CountriesView
的CountriesFragment
如下:
public class CountriesFragment
extends MvpLceFragment<SwipeRefreshLayout, List<Country>, CountriesView, CountriesPresenter>
implements CountriesView, SwipeRefreshLayout.OnRefreshListener {
@Bind(R.id.recyclerView) RecyclerView recyclerView;
CountriesAdapter adapter;
@Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.countries_list, container, false);
}
@Override public void onViewCreated(View view, @Nullable Bundle savedInstance) {
super.onViewCreated(view, savedInstance);
// Setup contentView == SwipeRefreshView
contentView.setOnRefreshListener(this);
// Setup recycler view
adapter = new CountriesAdapter(getActivity());
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
recyclerView.setAdapter(adapter);
loadData(false);
}
public void loadData(boolean pullToRefresh) {
presenter.loadCountries(pullToRefresh);
}
@Override protected CountriesPresenter createPresenter() {
return new SimpleCountriesPresenter();
}
@Override public void setData(List<Country> data) {
adapter.setCountries(data);
adapter.notifyDataSetChanged();
}
@Override public void onRefresh() {
loadData(true);
}
}
沒有太多的代碼要寫,對(duì)吧洁桌?這是因?yàn)榛?code>MvpLceFragment已經(jīng)幫我們實(shí)現(xiàn)了從加載視圖切換到內(nèi)容視圖或者錯(cuò)誤視圖渴丸。乍一看你可能會(huì)被MvpLceFragment
那一串泛型參數(shù)列表嚇到。讓我解釋一下:第一個(gè)泛型參數(shù)是content view 的類型(從android.view.View延伸的東西)另凌。第二個(gè)是fragment要顯示的Model谱轨。第三個(gè)是View接口,最后一個(gè)是Presenter的類型吠谢⊥镣總結(jié):MvpLceFragment<AndroidView, Model, View, Presenter>
ViewGroup
如果你想避免使用Fragment,你可以做到這一點(diǎn)工坊。Mosby為ViewGroups
提供了與Activities and Fragments相同的MVP腳手架献汗。API與Activity和Fragment的相同。一些默認(rèn)的實(shí)現(xiàn)像MvpFrameLayout
王污,MvpLinearLayout
和MvpRelativeLayout
已經(jīng)提供使用了罢吃。
Delegation委托
您可能想知道,Mosby如果不使用代碼復(fù)制(復(fù)制和粘貼相同的代碼)昭齐,是如何為所有類型的view(Activity尿招,F(xiàn)ragment和ViewGroup)提供相同的API的。答案是delegation委托阱驾。委托的方法已被命名為與Activity或Fragments生命周期的方法名稱(受appcompat支持庫(kù)中最新的AppCompatDelegate的啟發(fā))相匹配的名稱泊业,以更好地理解應(yīng)從哪個(gè)Activity或Fragment生命周期方法調(diào)用哪個(gè)委托方法:
MvpDelegateCallback
:是每個(gè)Mosby中的MvpView
都必須實(shí)現(xiàn)的接口“∫祝基本上它只是提供了一些MVP相關(guān)的方法像createPresenter()
等吁伺。這個(gè)方法在內(nèi)部被ActivityMvpDelegate
或FragmentMvpDelegate
調(diào)用。ActivityMvpDelegate
:這是一個(gè)接口租谈。通常你使用ActivityMvpDelegateImpl
這個(gè)默認(rèn)的實(shí)現(xiàn)篮奄。要想在你自己的Activity中引入Mosby MVP捆愁,你需要做的是,從Activity的onCreate()
窟却,onPause()
昼丑,onDestroy()
等生命周期方法中調(diào)用相應(yīng)的委托方法,并實(shí)現(xiàn)MvpDelegateCallback
:
public abstract class MyActivity extends Activity implements MvpDelegateCallback<> {
protected ActivityMvpDelegate mvpDelegate = new ActivityMvpDelegateImpl(this);
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mvpDelegate.onCreate(savedInstanceState);
}
@Override protected void onDestroy() {
super.onDestroy();
mvpDelegate.onDestroy();
}
@Override protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
mvpDelegate.onSaveInstanceState(outState);
}
... // other lifecycle methods
}
FragmentMvpDelegate
:同ActivityMvpDelegate
一樣夸赫。要在你的Fragment中引入Mosby MVP的支持菩帝,您所要做的就和上面在Activity中引入一樣:創(chuàng)建一個(gè)FragmentMvpDelegate
,并從Fragment的生命周期方法中調(diào)用相應(yīng)的委托方法茬腿,你的Fragment同樣也必須實(shí)現(xiàn)MvpDelegateCallback
呼奢。通常你可以使用默認(rèn)的委托實(shí)現(xiàn)FragmentMvpDelegateImpl
ViewGroupMvpDelegate
:這個(gè)委托是給ViewGroup
用的。在你的ViewGroup
中引入Mosby MVP切平,生命周期方法要比Fragment的更簡(jiǎn)單:onAttachedToWindow()
和onDetachedFromWindow()
握础。默認(rèn)的實(shí)現(xiàn)是ViewGroupMvpDelegateImpl
。
委托的另一個(gè)優(yōu)點(diǎn)是可以將Mosby整合到其他任何一個(gè)第三方庫(kù)或框架中悴品。只需實(shí)現(xiàn)MvpDelegateCallback
并實(shí)例化一個(gè)委托禀综,并在生命周期事件中調(diào)用相應(yīng)的委托方法。
演示模型
在理想世界中苔严,我們通過最佳的方式得到在我們的GUI(View)中顯示的數(shù)據(jù)定枷。很多時(shí)候,我們通過公共API檢索后端數(shù)據(jù)届氢,這些公共API無法為了適應(yīng)UI的需求而更改依鸥。實(shí)際上,后端根據(jù)你的用戶界面提供一個(gè)API并不是一個(gè)好主意悼沈,因?yàn)槿绻愀淖兡愕挠脩艚缑婕伲阋部赡苄枰淖兒蠖恕R虼诵豕仨殞odel轉(zhuǎn)換衣吠,從而使你的GUI可以輕松的顯示。一個(gè)典型的例子是從一個(gè)REST json API中加載一個(gè)Items列表壤靶,比方說一個(gè)用戶列表缚俏,并將它們顯示在一個(gè)ListView中。使用MVP贮乳,在真實(shí)環(huán)境中這個(gè)工作是這樣的:
[圖片上傳失敗...(image-94dd30-1513151241674)]
這里沒有新東西忧换。List<User>
被加載并且GUI 通過使用 UserAdapter
在ListView
中顯示用戶。我敢肯定向拆,你之前已經(jīng)千萬次的使用了ListView
和Adapter
亚茬,但你可曾想過背后的想法Adapter
?Adapter通過android UI控件使你的model可以顯示出來浓恳。這就是適配器設(shè)計(jì)模式adapter design pattern刹缝。如果我們想要支持手機(jī)和平板電腦碗暗,還都以不同的方式顯示item呢?我們是實(shí)現(xiàn)兩個(gè)適配器:PhoneUserAdapter
和TabletUserAdapter
梢夯,然后在運(yùn)行時(shí)選擇合適的適配器么言疗。
如果那樣做,就真是“理想情況”了颂砸。如果我們必須對(duì)用戶列表進(jìn)行排序或者顯示一些必須通過復(fù)雜(和CPU密集型)方式進(jìn)行計(jì)算的事情呢噪奄?我們不能在UserAdapter
中那樣做,因?yàn)樵谥鱑I線程上做那些繁重的工作會(huì)導(dǎo)致listview滾動(dòng)性能問題人乓。因此勤篮,我們放到一個(gè)單獨(dú)的線程中去做。隨之而來的有兩個(gè)問題:第一個(gè)是我們?nèi)绾无D(zhuǎn)換數(shù)據(jù)撒蟀?我們拿我們的用戶類,并添加一些額外的屬性么温鸽?我們是否覆蓋用戶類的值保屯?
public class User {
String firstname;
String lastname;
}
我們假設(shè)我們UserView
想要顯示全名,并計(jì)算一個(gè)排名使列表排序:
public class User {
String firstname;
String lastname;
int ranking;
public String getFullname(){
return firstname +" "+lastname;
}
}
雖然引入方法getFullname()
是可以的涤垫,但添加ranking
字段可能會(huì)導(dǎo)致問題姑尺,想象一下我們從后端檢索得到的User
可能并沒有ranking
。所以首先蝠猬,如果你看看你的json api提要切蟋,并將它與我們的User類進(jìn)行比較,最后但不是最不重要的ranking 將設(shè)為默認(rèn)值零榆芦,因?yàn)槲覀冞€沒有計(jì)算出排名柄粹。如果我們使用了一個(gè)對(duì)象而不是一個(gè)整數(shù),那么默認(rèn)值就是null匆绣,并且很可能會(huì)遇到NullPointerException驻右。
解決方案是引入一個(gè) Presentation Model。這個(gè)模型只是為我們的GUI優(yōu)化的一個(gè)類:
public class UserPresentationModel {
String fullname;
int ranking;
public UserPresentationModel(String fullname, int ranking) { ... }
}
通過這樣做崎淳,我們確定ranking
始終被設(shè)置為一個(gè)具體值堪夭,并且在滾動(dòng)ListView時(shí)不會(huì)計(jì)算fullname(PresentationModel在獨(dú)立線程中實(shí)例化)。UserView現(xiàn)在顯示List<UserPresentationModel>
而不是List<User>
拣凹。
第二個(gè)問題是:在哪里做異步轉(zhuǎn)換森爽?View, Model 還是 Presenter? 很明顯,View進(jìn)行這種轉(zhuǎn)換操作嚣镜,因?yàn)閂iew知道如何在屏幕上顯示事物爬迟。
[圖片上傳失敗...(image-4a5c8d-1513151241674)]
PresentationModelTransformer
是接受List<User>
并將其“轉(zhuǎn)換”到List<UserPresentationModel>
的組件(適配器模式,所以我們有兩個(gè)adapter:一個(gè)轉(zhuǎn)換為表示模型菊匿,另一個(gè)是在ListView中顯示它們的UserAdapter)雕旨。在view中整合PresentationModelTransformer
的優(yōu)勢(shì)在于扮匠,view知道如何顯示內(nèi)容,并且可以在內(nèi)部輕松切換 手機(jī)和平??板電腦優(yōu)化了的演示模型(可能平板電腦的用戶界面跟手機(jī)比還有其他需求)凡涩。但是棒搜,最大的缺點(diǎn)是現(xiàn)在view必須控制異步線程和視圖狀態(tài)(在進(jìn)行轉(zhuǎn)換時(shí)顯示ProgressBar?;罨力麸?),這顯然是Presenter的工作育韩。因此克蚂,讓轉(zhuǎn)換成為view的一部分并不是一個(gè)好主意。在Presenter中包括轉(zhuǎn)換是將要做的:
[圖片上傳失敗...(image-bad7b2-1513151241674)]
正如我們前面已經(jīng)討論的那樣筋讨,Presenter
負(fù)責(zé)協(xié)調(diào)View埃叭,因此Presenter告訴view在UserPresentationModel
轉(zhuǎn)換完成后顯示ListView 。此外悉罕,Presenter可以控制所有異步線程(轉(zhuǎn)換的異步線程)赤屋,并在必要時(shí)取消它們。順便說一下:使用RxJava
壁袄,你可以使用類似map()
或者flatMap()
操作符進(jìn)行轉(zhuǎn)換类早。如果我們想要支持手機(jī)和平板電腦,我們可以定義兩個(gè)實(shí)現(xiàn)了不同PresentationModelTransformer
的Presenter PhoneUserPresenter
和TabletUserPresenter
嗜逻。在Mosby涩僻,View創(chuàng)建Presenter。由于在運(yùn)行時(shí)View知道是手機(jī)還是平板電腦栈顷,因此可以在運(yùn)行時(shí)選擇不同的Presenter實(shí)例化(PhoneUserPresenter或TabletUserPresenter)逆日。或者萄凤,你可以為手機(jī)和平板電腦使用同一個(gè)UserPresenter
屏富,僅通過使用依賴注入替換PresentationModelTransformer
的實(shí)現(xiàn)。