Mosby翻譯(二):MVP原理

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)了MvpViewMvpActivityMvpFragment用作基類奋蔚。為了確保類型安全浸颓,建議這樣使用: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 MvpLceViewMvpLceFragment implements MvpLceView來實(shí)現(xiàn)。這兩個(gè)都假設(shè)XML布局中包含了含有R.id.loadingView沉桌,R.id.contentViewR.id.errorView的view谢鹊。

示例

在下面的示例中(托管在Github上),我們通過使用CountriesAsyncLoader加載Country列表并在Fragment的RecyclerView中顯示留凭。

我們首先定義視圖界面CountriesView

public interface CountriesView extends MvpLceView<List<Country>> {
}

為什么我需要為View定義接口佃扼?

  1. 由于它是一個(gè)接口,你可以改變view的實(shí)現(xiàn)蔼夜。我們可以簡(jiǎn)單的將代碼從繼承Activity的實(shí)現(xiàn)中拷貝到繼承Fragment的實(shí)現(xiàn)中兼耀。

  2. 模塊化:您可以將整個(gè)業(yè)務(wù)邏輯,Presenter和View Interface移動(dòng)到獨(dú)立的庫(kù)中求冷。然后瘤运,把這個(gè)包含了Presenter的庫(kù)應(yīng)用到各種app中。

  3. 您可以輕松編寫單元測(cè)試匠题,因?yàn)槟梢酝ㄟ^實(shí)現(xiàn)view interface來模擬視圖拯坟。還有一個(gè)更簡(jiǎn)單的方法就是在presenter中引入java接口并模擬presenter對(duì)象來編寫單元測(cè)試。

  4. 為視圖定義一個(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)CountriesViewCountriesFragment如下:

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王污,MvpLinearLayoutMvpRelativeLayout已經(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)部被ActivityMvpDelegateFragmentMvpDelegate調(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 通過使用 UserAdapterListView中顯示用戶。我敢肯定向拆,你之前已經(jīng)千萬次的使用了ListViewAdapter亚茬,但你可曾想過背后的想法Adapter?Adapter通過android UI控件使你的model可以顯示出來浓恳。這就是適配器設(shè)計(jì)模式adapter design pattern刹缝。如果我們想要支持手機(jī)和平板電腦碗暗,還都以不同的方式顯示item呢?我們是實(shí)現(xiàn)兩個(gè)適配器:PhoneUserAdapterTabletUserAdapter梢夯,然后在運(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 PhoneUserPresenterTabletUserPresenter嗜逻。在Mosby涩僻,View創(chuàng)建Presenter。由于在運(yùn)行時(shí)View知道是手機(jī)還是平板電腦栈顷,因此可以在運(yùn)行時(shí)選擇不同的Presenter實(shí)例化(PhoneUserPresenter或TabletUserPresenter)逆日。或者萄凤,你可以為手機(jī)和平板電腦使用同一個(gè)UserPresenter屏富,僅通過使用依賴注入替換PresentationModelTransformer的實(shí)現(xiàn)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蛙卤,一起剝皮案震驚了整個(gè)濱河市狠半,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌颤难,老刑警劉巖神年,帶你破解...
    沈念sama閱讀 218,525評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異行嗤,居然都是意外死亡已日,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門栅屏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來飘千,“玉大人堂鲜,你說我怎么就攤上這事』つ危” “怎么了缔莲?”我有些...
    開封第一講書人閱讀 164,862評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)霉旗。 經(jīng)常有香客問我痴奏,道長(zhǎng),這世上最難降的妖魔是什么厌秒? 我笑而不...
    開封第一講書人閱讀 58,728評(píng)論 1 294
  • 正文 為了忘掉前任读拆,我火速辦了婚禮,結(jié)果婚禮上鸵闪,老公的妹妹穿的比我還像新娘檐晕。我一直安慰自己,他們只是感情好蚌讼,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,743評(píng)論 6 392
  • 文/花漫 我一把揭開白布辟灰。 她就那樣靜靜地躺著,像睡著了一般啦逆。 火紅的嫁衣襯著肌膚如雪伞矩。 梳的紋絲不亂的頭發(fā)上笛洛,一...
    開封第一講書人閱讀 51,590評(píng)論 1 305
  • 那天夏志,我揣著相機(jī)與錄音,去河邊找鬼苛让。 笑死沟蔑,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的狱杰。 我是一名探鬼主播瘦材,決...
    沈念sama閱讀 40,330評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼仿畸!你這毒婦竟也來了食棕?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,244評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤错沽,失蹤者是張志新(化名)和其女友劉穎簿晓,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體千埃,經(jīng)...
    沈念sama閱讀 45,693評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡憔儿,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,885評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了放可。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片谒臼。...
    茶點(diǎn)故事閱讀 40,001評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡朝刊,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蜈缤,到底是詐尸還是另有隱情拾氓,我是刑警寧澤,帶...
    沈念sama閱讀 35,723評(píng)論 5 346
  • 正文 年R本政府宣布劫樟,位于F島的核電站痪枫,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏叠艳。R本人自食惡果不足惜奶陈,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,343評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望附较。 院中可真熱鬧吃粒,春花似錦、人聲如沸拒课。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,919評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)早像。三九已至僻肖,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間卢鹦,已是汗流浹背臀脏。 一陣腳步聲響...
    開封第一講書人閱讀 33,042評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留冀自,地道東北人揉稚。 一個(gè)月前我還...
    沈念sama閱讀 48,191評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像熬粗,于是被迫代替她去往敵國(guó)和親搀玖。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,955評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容