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)]

查詢和顯示來(lái)自數(shù)據(jù)庫(kù)的用戶列表的具體工作流程:

[圖片上傳失敗...(image-190b62-1513151241674)]

上面顯示的工作流程圖應(yīng)該是很容易理解的。不過(guò)這里有一些額外的想法:

  • Presenter 并不是OnClickListener。View負(fù)責(zé)處理用戶輸入并調(diào)用Presenter 的相應(yīng)方法。為什么不通過(guò)將Presenter變成OnClickListener從而消除這種"轉(zhuǎn)移"的過(guò)程呢?如果這樣做,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)。從理論上說(shuō),Presenter和業(yè)務(wù)邏輯應(yīng)該能夠在桌面程序或其他java程序間共享的普通java代碼。

  • 就像在步驟1和步驟2中看到的,View只做Presenter告訴View 需要做的那些操作:用戶點(diǎn)擊了“l(fā)oad user button”(第1步)之后,view不會(huì)直接顯示加載動(dòng)畫(huà)。而是在步驟2由Presenter明確地告訴view去顯示加載動(dòng)畫(huà)。Model-View-Presenter的這種變體被稱為被動(dòng)視圖(Passive View)。view應(yīng)該盡可能愚蠢。讓Presenter以抽象的方式控制view。例如:Presenter調(diào)用view.showLoading(),而不是控制view中特定的東西,如動(dòng)畫(huà)。所以,Presenter不應(yīng)該調(diào)用view.startAnimation()這種方法。

  • 通過(guò)實(shí)現(xiàn)MVP被動(dòng)視圖,處理并發(fā)和多線程更容易。就像您在步驟3中看到的那樣,數(shù)據(jù)庫(kù)查詢異步運(yùn)行,Presenter是一個(gè)監(jiān)聽(tīng)器Listener/觀察者Observer,并在數(shù)據(jù)準(zhǔn)備好顯示時(shí)得到通知。

Android上的MVP

到現(xiàn)在為止還挺好。但是如何在自己的Android應(yīng)用上應(yīng)用MVP?第一個(gè)問(wèn)題是,我們應(yīng)該在哪里應(yīng)用MVP模式?在Activity上,F(xiàn)ragment上,還是在像RelativeLayout這樣的ViewGroup上?讓我們來(lái)看看Gmail Android平板應(yīng)用程序:

[圖片上傳失敗...(image-c3a04-1513151241674)]

在我們看來(lái),在上圖所示的屏幕上有四個(gè)獨(dú)立的可使用MVP的地方?!翱梢允褂肕VP的地方”是指屏幕上顯示的、在邏輯上屬于一個(gè)整體的UI元素。因此這些地方也可以稱為是可以運(yùn)用MVP的一個(gè)單獨(dú)的UI單元。

[圖片上傳失敗...(image-b543c2-1513151241674)]

這樣看起來(lái)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,來(lái)顯示從本地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)畫(huà)(ProgressBar),之后用ListView替換它。你會(huì)把所有的代碼放入Fragment嗎?如果加載時(shí)出現(xiàn)了錯(cuò)誤怎么辦?如果屏幕方向改變了呢?誰(shuí)負(fù)責(zé)取消AsyncTasks?這一系列的問(wèn)題都可以用MVP來(lái)解決。讓我們向1000+行、大雜燴似的Activity和Fragment代碼說(shuō)再見(jiàn)吧。

但是在我們深入了解如何在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é)。你可以說(shuō)這些聽(tīng)起來(lái)Activity和Fragment更像是一個(gè)Controller。然而,我們得出的結(jié)論是Activity和Fragment應(yīng)該被視為(愚蠢的)View,而不是Presenter。后面你會(huì)看到原因。

有了這個(gè)說(shuō)法,我們想要介紹Mosby,這是一個(gè)在android上創(chuàng)建基于MVP的應(yīng)用程序的庫(kù)。

Mosby

你可能已經(jīng)發(fā)現(xiàn),如果你試圖去解釋MVP是MVC(Model-View-Controller)的變種或改進(jìn),那么就很難理解什么是Presenter。尤其是iOS開(kāi)發(fā)人員,他們很難理解Controller和Presenter的區(qū)別, because they “grew up” with the fixed idea and definition of an iOS alike UIViewController。在我們來(lái)看,MVP并不是MVC的變種或改進(jìn),因?yàn)檫@意味著Presenter取代了Controller。我們認(rèn)為,MVP包裝了MVC??纯茨闶褂肕VC開(kāi)發(fā)的app。通常你有你的View和Controller(即Android中的Fragment或iOS的UIViewController)處理點(diǎn)擊事件,綁定數(shù)據(jù)和觀察ListView(或在iOS上為UITableView實(shí)現(xiàn)一個(gè)UITableViewDelegate)等等?,F(xiàn)在退一步,想象一下,controller就是view的一部分,而不是直接連接到你的model(業(yè)務(wù)邏輯)。而Presenter位于controller 和model的中間,如下所示:

[圖片上傳失敗...(image-821785-1513151241674)]

讓我們來(lái)看一個(gè)具體的例子:示例程序顯示從數(shù)據(jù)庫(kù)中查詢的用戶列表。當(dāng)用戶點(diǎn)擊“加載按鈕”時(shí)開(kāi)始執(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)畫(huà)的組件,如隱藏ProgressBar并顯示ListView。Controller監(jiān)聽(tīng)ListView上的滾動(dòng)事件,即在滾動(dòng)ListView時(shí)進(jìn)行一些item動(dòng)畫(huà)或顯示隱藏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)畫(huà),然后在數(shù)據(jù)準(zhǔn)備好后,顯示ListView。

MvpView和MvpPresenter

所有view的基類是MvpView。本質(zhì)上它只是一個(gè)空的interface。該接口為Presenter提供了一個(gè)公共API來(lái)調(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。這樣一來(lái)Mosby獲取到Activity和Fragment的生命周期(更多內(nèi)容可以查看下面“委托”部分的內(nèi)容)。因此,初始化和清理東西(如取消異步運(yùn)行任務(wù))的操作應(yīng)該在presenter.attachView()presenter.detachView()中執(zhí)行。

Mosby提供了Presenter的另一種實(shí)現(xiàn)MvpBasePresenter,它使用WeakReference來(lái)保存對(duì)view(Fragment or Activity)的引用,以避免內(nèi)存泄漏。因此,當(dāng)你的Presenter想要調(diào)用view的方法時(shí),您必須通過(guò)調(diào)用isViewAttached()來(lái)檢查這個(gè)view是否被關(guān)聯(lián)到你的Presenter,并通過(guò)使用
getView()來(lái)或者view的引用。

另外,你可以為你的MvpView使用實(shí)現(xiàn)了空對(duì)象模式的MvpNullObjectBasePresenter。所以無(wú)論什么時(shí)候MvpNullObjectBasePresenter.onDetach()被調(diào)用,view都不會(huì)被設(shè)置為null(像MvpBasePresenter這樣),而是通過(guò)使用反射來(lái)動(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)用程序中一遍又一遍地寫(xiě)同樣的東西:在后臺(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);
}

上面說(shuō)的那種view,你可以使用MvpLceActivity implements MvpLceViewMvpLceFragment implements MvpLceView來(lái)實(shí)現(xiàn)。這兩個(gè)都假設(shè)XML布局中包含了含有R.id.loadingView,R.id.contentViewR.id.errorView的view。

示例

在下面的示例中(托管在Github上),我們通過(guò)使用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. 您可以輕松編寫(xiě)單元測(cè)試,因?yàn)槟梢酝ㄟ^(guò)實(shí)現(xiàn)view interface來(lái)模擬視圖。還有一個(gè)更簡(jiǎn)單的方法就是在presenter中引入java接口并模擬presenter對(duì)象來(lái)編寫(xiě)單元測(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),我們可以說(shuō),這是非常有用的,特別是如果你在一個(gè)團(tuán)隊(duì)中工作。

請(qǐng)注意,我們也可以使用MvpLceView<List<Country>>,而不是定義一個(gè)(空的,因?yàn)槔^承方法)接口CountriesView。但是有一個(gè)專用的接口CountriesView可以提高代碼的可讀性,而且我們可以在將來(lái)更靈活地定義更多的與View有關(guān)的方法。

接下來(lái)我們用所需的id來(lái)定義我們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);
  }
}

沒(méi)有太多的代碼要寫(xiě),對(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的類型??偨Y(jié):MvpLceFragment<AndroidView, Model, View, Presenter>

ViewGroup

如果你想避免使用Fragment,你可以做到這一點(diǎn)。Mosby為ViewGroups提供了與Activities and Fragments相同的MVP腳手架。API與Activity和Fragment的相同。一些默認(rèn)的實(shí)現(xiàn)像MvpFrameLayoutMvpLinearLayoutMvpRelativeLayout已經(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)的接口?;旧纤皇翘峁┝艘恍㎝VP相關(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)的委托方法。

演示模型

在理想世界中,我們通過(guò)最佳的方式得到在我們的GUI(View)中顯示的數(shù)據(jù)。很多時(shí)候,我們通過(guò)公共API檢索后端數(shù)據(jù),這些公共API無(wú)法為了適應(yīng)UI的需求而更改。實(shí)際上,后端根據(jù)你的用戶界面提供一個(gè)API并不是一個(gè)好主意,因?yàn)槿绻愀淖兡愕挠脩艚缑妫阋部赡苄枰淖兒蠖?。因此,您必須將model轉(zhuǎn)換,從而使你的GUI可以輕松的顯示。一個(gè)典型的例子是從一個(gè)REST json API中加載一個(gè)Items列表,比方說(shuō)一個(gè)用戶列表,并將它們顯示在一個(gè)ListView中。使用MVP,在真實(shí)環(huán)境中這個(gè)工作是這樣的:

[圖片上傳失敗...(image-94dd30-1513151241674)]

這里沒(méi)有新東西。List<User>被加載并且GUI 通過(guò)使用 UserAdapterListView中顯示用戶。我敢肯定,你之前已經(jīng)千萬(wàn)次的使用了ListViewAdapter,但你可曾想過(guò)背后的想法Adapter?Adapter通過(guò)android UI控件使你的model可以顯示出來(lái)。這就是適配器設(shè)計(jì)模式adapter design pattern。如果我們想要支持手機(jī)和平板電腦,還都以不同的方式顯示item呢?我們是實(shí)現(xiàn)兩個(gè)適配器:PhoneUserAdapterTabletUserAdapter,然后在運(yùn)行時(shí)選擇合適的適配器么。

如果那樣做,就真是“理想情況”了。如果我們必須對(duì)用戶列表進(jìn)行排序或者顯示一些必須通過(guò)復(fù)雜(和CPU密集型)方式進(jìn)行計(jì)算的事情呢?我們不能在UserAdapter中那樣做,因?yàn)樵谥鱑I線程上做那些繁重的工作會(huì)導(dǎo)致listview滾動(dòng)性能問(wèn)題。因此,我們放到一個(gè)單獨(dú)的線程中去做。隨之而來(lái)的有兩個(gè)問(wèn)題:第一個(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)致問(wèn)題,想象一下我們從后端檢索得到的User可能并沒(méi)有ranking。所以首先,如果你看看你的json api提要,并將它與我們的User類進(jìn)行比較,最后但不是最不重要的ranking 將設(shè)為默認(rèn)值零,因?yàn)槲覀冞€沒(méi)有計(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) { ... }
}

通過(guò)這樣做,我們確定ranking始終被設(shè)置為一個(gè)具體值,并且在滾動(dòng)ListView時(shí)不會(huì)計(jì)算fullname(PresentationModel在獨(dú)立線程中實(shí)例化)。UserView現(xiàn)在顯示List<UserPresentationModel>而不是List<User>。

第二個(gè)問(wèn)題是:在哪里做異步轉(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í)取消它們。順便說(shuō)一下:使用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,僅通過(guò)使用依賴注入替換PresentationModelTransformer的實(shí)現(xiàn)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容