Ted Mosby - 軟件架構

我給這篇關于Android庫的博客起的名字靈感來源于《老爸老媽浪漫史》中的建筑設計師Ted Mosby。這個Mosby庫可以幫助大家在Android上通過Model-View-Presenter模式做出一個完善穩健、可重復使用的軟件,還可以借助ViewState輕松實現屏幕翻轉。

Model-View-Presenter (MVP)

MVP模式是一個把view從低層模型分離出來的一種現代模式。MVP由model–view–controller (MVC)軟件模式衍生而來,常用于構建UI

  • MVP中的M(model)代表的是將會顯示在view(UI)中的數據。
  • MVP中的V(view)是顯示數據(model)并且將用戶指令(events)傳送到presenter以便作用于那些數據的一個接口。View通常含有Presenter的引用。
  • MVP中的P(presenter)扮演的是“中間人”的作用(就如MVC中的controller),且presenter同時引用view和model。值得注意的是,“Model”這個詞并不正確。嚴格意義上來說,它指的應該是檢索或控制一個Model的業務邏輯層。舉個例子,比如你的數據庫里面包含了User,而你的View想要顯示一個User列表,那么Presenter會引用數據庫中的業務邏輯層(比如DAO)從而查詢到一個User列表。如圖1-1.

從數據庫中查詢或顯示User列表的具體流程如圖1-2:

以上工作流程圖應該能夠說明問題了。但是,還有以下幾點值得注意的地方:

  • Presenter不是一個OnClickListenerView主要是負責處理用戶輸入并調用presenter相應的方法。那么問題來了,為什么不把Presenter?直接做成一個OnClickListener,從而把“轉發流程”給省略掉呢?大家想想,如果這樣做的話,首先,presenter需要知道view的內部構件。舉個例子,如果一個View有兩個按鈕,且這個view在這兩個按鈕上都把Presenter?注冊成OnClickListener的話,那么發生點擊事件時Presenter?(在不知道view中按鈕引用等內部構件的情況下)怎么能夠區分出是哪一個按鈕被點擊了呢?Model,View和Presenter三者應解耦。其次,如果讓Presenter?執行OnClickListener,Presenter就被綁定到了Android平臺上。理論上來說presenter和業務邏輯層都是純舊式的能夠與桌面應用或其他任何java應用共享的java代碼。

  • 大家在第1步和第2步中可以看到,View?只執行Presenter?指示的操作:用戶點擊“load user button”(第1步)后,view并沒有直接顯示加載動畫,而是在第2步presenter明確告訴其顯示加載動畫后才顯示的。這一Model-View-Presenter的變體稱之為MVP 被動視圖。這個view可以說是要多笨有多笨。這時我們需要讓presenter以一種更抽象的方式來控制view。比如,presenter在調用?view.showLoading()?時并不控制view的諸如動畫等具體事項。所以presenter不應調用view.startAnimation()?等方法。

  • 通過執行MVP被動視圖,并發性以及多線程更容易處理。大家可以看到,第3步中數據庫查詢異步運行,并且presenter作為Listener/Observer,在數據準備顯示時presenter收到通知。

Android上的MVP

目前為止一切順利。但是大家怎么樣把MVP運用到自己的Android 應用上呢?第一個問題在于,我們要把MVP模式運用到什么地方?Activity上、Fragment上、還是像RelativeLayout這類的ViewGroup上?我們來看看Android平板上的Gmail應用,如圖1-3:

在我看來,上圖屏幕中有四個可以使用MVP的地方。我所說的“可以使用MVP的地方”是指屏幕上顯示的、在邏輯上屬于一個整體的UI元素。因此這些地方也可以稱為是可以運用MVP的一個單獨的UI單元。如圖 1-4.

看起來MVP似乎很適合運用到Activity,特別是Fragment上。通常Fragment只負責顯示單一的如ListView之類的內容,就像依靠MailProvider 來獲取一系列MailsInboxPresenter?控制下的?InboxView一樣。但是,MVP不僅僅限于Fragment或Activity,它還可以運用到SearchView中顯示的ViewGroup中。在我的大多數app里面我都在Fragment運用MVP模式。但是大家可以自行決定把MVP運用到什么地方,前提是view是獨立的,這樣這樣presenter才能在不與其他Presenter沖突的情況下控制View。

我們為什么要實現MVP

我們如何在不使用MVP模式時顯示Email列表到Fragment? 通常,我們需要獲取并且合并本地SQL數據庫和從IMAP郵件服務器獲取的郵件列表,然后將郵件列表綁定到收件箱view中。那么,此時fragment的代碼又會是怎么樣的呢?我們需要運行兩個AsyncTasks?并實現一個“等待機制”(等到兩個任務將兩者的加載數據合并到一個單獨的mail列表)。我們還需要注意的是在加載時要顯示加載動畫(ProgressBar),之后用ListView替代。我們需要把所有的代碼放到Fragment中嗎?要是加載過程中出現錯誤怎么辦?屏幕翻轉怎么辦?誰來負責撤銷AsyncTasks??這一系列的問題都可以通過MVP得到解決。讓我們跟那些帶有上千行大雜燴代碼的activity和fragment說拜拜吧

但是,在我們深入研究如何將MVP運用到Android中之前,我們需要弄清楚的一個問題是:Activity或Fragment究竟是一個View還是一個Presenter。Activity或Fragment似乎既是View也是Presenter,因為它們都有?onCreate()?或onDestroy()之類的生命周期回調功能,并且它們負責從一個UI控件到另一個UI 控件的轉換(比如在加載時顯示ProgressBar,然后顯示帶有數據的ListView)等View操作。大家可能會覺得這里的Activity或Fragment就是一個Controller,我猜可能也是這么一個初衷。但是在經歷了幾年的Android應用開發之后,我得出這么一個結論:我們應該把Activity或Fragment看作是一個不太智能的View,而不是把它們看作一個Presenter。后文我會給出原因。

綜上,我想給大家介紹一個在Android平臺上開發基于MVP的應用的一個?Mosby庫。 Mosby

大家可以在Github和Maven Central上找到Mosby庫。Mosby分為幾個子模塊,大家可以根據自己的需要選取組件。我們來回顧一下最重要的一個模塊。

核心模塊 ( Core Module)

《老爸老媽浪漫史》中的建筑設計師Ted Mosby想建造一棟摩天大樓。而建造這樣一棟宏偉的建筑必須打好堅實的地基。這對Android應用的開發來說是也是一樣的道理。基本上,Core Module?分為兩種類型:MosbyActivity?和MosbyFragment。這兩者是所有其他activity或fragment子類的基類(相當于建筑的地基)。兩者都使用我們大家所熟知的APT (Annotation Processing Tool)來減少一些樣板式代碼。MosbyActivity?和MosbyFragment?使用Butterknife進行view的注入,使用Icepick?將實例狀態保存和存儲到Bundle中,使用FragmentArgs注入Fragment參數。我們不需要再調用Butterknife.inject(this)等插入方法。這類代碼已經包含在了MosbyActivity 和?MosbyFragment中。它是即時可用的。我們需要做的就是使用子類中相應的注解。核心模塊與MVP沒有關聯,它只是寫一個大型軟件的基礎。

MVP模塊( MVP Module )

Mosby庫中的MVP模塊使用泛型來確保類型安全。所有view的基類是MvpView。從根本上說這只是一個空的interface 。Presenter的基類是MvpPresenter

public interface MvpView{}

public interface MvpPresenter<V extends MvpView>{
    public void attachView(V view);
    public void detachView(boolean retainInstance);
}

上文提到,我們把ActivityFragment看做View。因此Mosby庫的MVP模塊提供了?屬于MvpViewsMvpActivityMvpFragment作為ActivityFragment的基類。

public abstract class MvpActivity<P extends MvpPresenter> extends MosbyActivity implements MvpView{

    protected P presenter;
    @Override  protected void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        presenter = createPresenter();
        presenter.attachView(this);
        super.onDestroy();
        presenter.detachView(false);
    }

    protected abstract PcreatePresenter();
}

public abstract class MvpFragment<P extends MvpPresenter> MosbyFragment implements MvpView{
    protected Ppresenter;

    @Override public void onViewCreated(View view,@Nullable Bundle savedInstanceState){
        super.onViewCreated(view,savedInstanceState);
        // Create the presenter if needed
        if(presenter == null){
            presenter = createPresenter();
        }
        presenter.attachView(this);
    }

    @Override public void onDestroyView(){
        super.onDestroyView();
        presenter.detachView(getRetainInstance());
    }

    protected abstract PcreatePresenter();
    }
}

@Override protected void onDestroy(){

這一理念主要是一個MvpView?(也就是Fragment or Activity)會關聯一個MvpPresenter,并且管理MbpPresenter的聲明周期。大家從上面的代碼片段可以看到,Mosby使用Activity和Fragement生命周期來實現這一目的。通常presenter是綁定在該生命周期上的。所以初始化或者清理一些東西等操作(例如撤銷異步運行任務)應該在?presenter.onAttach()和?presenter.onDetach()上進行。我們稍后會談到presenter如何使用setRetainInstanceState(true) “避開”Fragment中的生命周期。我相信大家也注意到了,?MvpPresenter是一個interface 。MVP模塊提供一個?MvpBasePresenter,這個MvpBasePresenter只持有View(是一個Fragment或Activity)的弱引用,從而避免內存泄露。因此,當presenter想要調用view方法時,我們需要查看isViewAttached()?并使用getView()來獲取引用,以檢查view是否連接到了presenter。

Loading-Content-Error (LCE)

通常Fragment會一直重復做某一件事。它在后臺加載數據,同時顯示加載view(即ProgressBar),并在屏幕上顯示加載的數據,或者當加載失敗時顯示view錯誤。如今,下拉刷新支持很容易實現,因為SwipeRefreshLayout是Android支持庫的組成部分。為了避免重復執行這一工作流,Mosby庫的MVP模塊提供了MvpLceView

public interface MvpLceView<M> extends MvpView{
    /**
       * 顯示一個加載中的視圖
       * loading view 必須有個id 為 R.id.loadingView的View
       * @param pullToRefresh 如果是true,那么表示下拉刷新被觸發了
       */
    public void showLoading(boolean pullToRefresh);
    /**
       * 顯示 content view.
       * <content view 的id必須是R.id.contentView
       */
    public void showContent();

    /**
       * 顯示錯誤信息
       * @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。兩者均假設解析的xml布局包括了含有R.id.loadingView,R.id.contentView和R.id.errorView的view

示例 接下來要舉的例子Github上也有中,我們使用CountriesAsyncLoader加載一系列的Country,并將其顯示在Fragment的RecyclerView中。大家可以從這個鏈接 https://db.tt/ycrCwt1L下載。

首先我們要定義CountriesView這一view interface 。

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

為什么要為View定義接口呢? 1.因為定義了這個接口之后我們可以更改view的實現。我們可以簡單地把代碼從一個繼承自 Activity的實現轉移到繼承自 Fragment的實現。

2.模塊性:我們可以移動獨立的庫項目中的整個業務邏輯層、Presenter以及View 接口,然后把這個包含了Presenter的庫應用到各類app當中。下圖中左側是使用了嵌入在ViewPager中的Activity的kicker app,以及使用嵌入在ViewPager中的Fragment的meinVerein app,如圖1-5。 兩者采用的是同一個定義了View接口和Presenter且測試了單元的庫。

由于我們可以通過執行view接口來模擬view,所以我們可以很容易地編寫單元測試。還有一個更簡單的方法就是在presenter中引入java接口并使用模擬presenter對象來編寫單元測試。 還有一個良性副作用就是,定義了view接口之后,我們不用直接從presenter再回調activity/fragment方法。我們這樣區分開來是因為在執行presenter時我們在IDE自動完成上看到的方法只是關于view接口的方法。就我個人體會來說,我覺得這個方法非常有用,特別是團隊一起工作的時候。需要注意的是,除了定義一個CountriesView接口之外,我們還可以采用MvpLceView<List>?。但是,定義一個專門的接口可以提高代碼可讀性,并且將來可以靈活地定義更多其他的與View相關的方法。

Next we define our views xml layout file with the required ids:

下一步我們需要按照指定的id來定義view xml 布局文件.

<FrameLayoutxmlns: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并運行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();
    }
}

實現CountriesView接口?的CountriesFragment?如下所示:

public class CountriesFragment
 extends MvpLceFragment<SwipeRefreshLayout,List<Country>,CountriesView,CountriesPresenter>
 implements CountriesView,SwipeRefreshLayout.OnRefreshListener{

    @InjectView(R.id.recyclerView)RecyclerViewrecyclerView;
    CountriesAdapteradapter;

    @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();
    }

    // Just a shorthand that will be called in onCreateView()
    @Override protected int getLayoutRes(){
        return R.layout.countries_list;
    }

    @Override public void setData(List<Country> data){
        adapter.setCountries(data);
        adapter.notifyDataSetChanged();
    }

    @Override public void onRefresh(){
        loadData(true);
    }
}

代碼數量也并不是很多嘛,對吧?這是因為基類已經執行了從加載view到content view或error view的轉換。我們可能第一眼看到那一列MvpLceFragment類屬參數會覺得灰心。但是我要解釋一下:第一種類屬參數代表的是content view的類型;第二種是指以fragment顯示的Model;第三種是View接口;最后一種是Presenter的類型。總結起來就是:MvpLceFragment<AndroidView, Model, View接口, Presenter>

大家可能還注意到的一個點就是?getLayoutRes(),它是MosbyFragment引入的用于解析xml view布局的速記法。

@Override public View onCreateView(LayoutInflater inflater,ViewGroup container,Bundle savedInstanceState){
    Return  inflater.inflate(getLayoutRes(),container,false);
}

因此,我們不用重寫onCreateView(),只需重寫getLayoutRes()。一般來說,onCreateView()只能創建view而onViewCreated()需要被重寫,以便為RecyclerView初始化Adapter等項。因此,千萬不要忘記調用super.OnViewCreated();

ViewState模塊

看到這里大家應該大概了解了如何運用Mosby庫。Mosby中的ViewState模塊能幫助我們在Android開發中解決一些棘手的難題:處理屏幕旋轉。

問:如果把正在運行country這個例子的app并顯示了一列country的設備從橫屏旋轉到豎屏,會出現什么情況?

答:大家到這個視頻鏈接https://youtu.be/9iSBGEIZmUw中看看,結果是一個新的?CountriesFragment會被實例化,app開始顯示ProgressBar(并重新加載country列表)而不再在RecyclerView中顯示country列表(屏幕旋轉前的狀態)

Mosby引入了ViewState來解決這個問題。原理就是,我們跟蹤presenter從關聯的View中調用的方法。比如,presenter調用的是view.showContent(),一旦showContent()被調用,view就會意識到其狀態變更為“showing content”,從而view把這一信息存儲到一個ViewState。如果view在方向改變過程中遭到破壞,那么ViewState 就會被存儲到Activity.onSaveInstanceState(Bundle)?或?Fragment.onSaveInstanceState(Bundle)中,并在Activity.onCreate(Bundle)?或Fragment.onActivityCreated(Bundle)中修復。

由于不是每種數據都能存儲在Bundle中,所以不同的數據類型采用不同的ViewState 實現:數據類型ArrayList采用ArrayListLceViewState;數據類型Parcelable 采用Parcelable DataLceViewState;數據類型Serializeable采用SerializeableLceViewState。如果使用的是一個可保持( Retaining )的Fragment,那么?ViewState在屏幕旋轉時不會被破壞,所以也就不需要存儲到Bundle中。因此,它可以存儲任何類型的數據。在這種情況下,我們需要使用RetainingFragmentLceViewState。存儲一個ViewState比較容易。由于我們的架構比較整潔,我們的View又有接口,ViewState?可以向presenter一樣通過調用同樣的接口方法來修復相關聯的view。舉個例子,MvpLceView一般有3種狀態,即:顯示showContent(),showLoading()和showError(),所以ViewState本身會調用相應的方法來修復view的狀態。

那只是一些內部構件。如果大家想編寫自定義的ViewState,了解以上內容就夠了。ViewStates的使用非常簡單。事實上,要把MvpLceFragment?遷移到MvpLceViewStateFragment?,我們只需要另外執行createViewState()?和?getData()。下面我們就在CountriesFragment中實踐一下吧:

public class CountriesFragment
 extends MvpLceViewStateFragment<SwipeRefreshLayout,List<Country>,CountriesView,CountriesPresenter>
 implements CountriesView,SwipeRefreshLayout.OnRefreshListener{

    @InjectView(R.id.recyclerView)RecyclerView recyclerView;
    CountriesAdapter adapter;

    @Override public LceViewState<List<Country>,CountriesView> createViewState(){
        return new RetainingFragmentLceViewState<List<Country>,CountriesView>(this);
    }

    @Override public List<Country> getData(){
        return adapter == null? null : adapter.getCountries();
    }

    // The code below is the same as before

    @Override public void onViewCreated(Viewview,@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();
    }

    // Just a shorthand that will be called in onCreateView()
    @Override protected int getLayoutRes(){
        return R.layout.countries_list;
    }

    @Override public void setData(List<Country> data){
        adapter.setCountries(data);
        adapter.notifyDataSetChanged();
    }

    @Override public void onRefresh(){
        loadData(true);
    }
}

以上就是全部過程啦。我們不必更改presenter或其他代碼。這里 是一個關于我們的獲得ViewState支持的CountriesFragment的視頻。在這個視頻中我們可以看到,view在方位轉變之后仍然處于同樣的“狀態”,即,view橫屏顯示country列表,隨后橫屏顯示country列表。View能橫屏顯示下拉刷新指示,變更為豎屏時也能顯示。

自定義ViewState

ViewState確實是一個強大且靈活的概念。看到這里我相信大家都了解了LCE (Loading-Content-Error) ViewState的易用性。下面我們就一起來編寫自己的View和ViewState吧。我們的View只顯示兩類不同的數據對象:A和B。結果應該像這個視頻 https://youtu.be/9iSBGEIZmUw 中演示的這樣:

大家心里肯定覺得,這也不怎么樣啊!別介啊,我只是想演示一下創建自己的ViewState是一件多么容易的事。

View 接口和數據對象(model)如下所示:

public class A implements Parcelable {
    String  name;

    public A(String  name){
        this.name=name;
    }

    public String  getName(){
        return name;
    }
}

public class B implements Parcelable {
    String  foo;

    public B(String  foo){
        this.foo=foo;
    }

    public String  getFoo(){
        return foo;
    }
}

public interface MyCustomView extends MvpView{

    public void showA(A a);

    public void showB(B b);
}

在這個簡單的例子中我們沒有加入業務邏輯層。因為我們假設在實際的app中如果有業務邏輯層的話會使整個生成A或B的操作變得復雜。Presenter如下所示:

public class MyCustomPresenter extends MvpBasePresenter<MyCustomView>{
    Random random = new Random();

    public void doA(){

        A a = new A("My name is A "+random.nextInt(10));

        if(isViewAttached()){
            getView().showA(a);
        }
    }

    public void doB(){
        B b = new B("I am B "+random.nextInt(10));

        if(isViewAttached()){
            getView().showB(b);
        }
    }
}

我們定義了實現了MyCustomView接口的MyCustomActivity

public class MyCustomActivity extends MvpViewStateActivity<MyCustomPresenter>
 implements MyCustomView{

    @InjectView(R.id.textViewA) TextViewaView;
    @InjectView(R.id.textViewB) TextViewbView;

    @Override protected void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        setContentView(R.layout.my_custom_view);
    }

    @Override public RestoreableViewState createViewState(){
        return new MyCustomViewState();// Our ViewState implementation
    }

    // Will be called when no view state exist yet,
    // which is the case the first time MyCustomActivity starts
    @Override public void onNew ViewStateInstance(){
        presenter.doA();
    }

    @Override protected MyCustomPresenter createPresenter(){
        return new MyCustomPresenter();
    }

    @Override public void showA(A a){
        MyCustomViewState vs = ((MyCustomViewState)viewState);
        vs.setShowingA(true);
        vs.setData(a);
        aView.setText(a.getName());
        aView.setVisibility(View.VISIBLE);
        bView.setVisibility(View.GONE);
    }

    @Override public void showB(B b){
        MyCustomViewState vs=((MyCustomViewState)viewState);
        vs.setShowingA(false);
        vs.setData(b);
        bView.setText(b.getFoo());
        aView.setVisibility(View.GONE);
        bView.setVisibility(View.VISIBLE);
    }

    @OnClick(R.id.loadA)public void onLoadAClicked(){
        presenter.doA();
    }

    @OnClick(R.id.loadB)public void onLoadBClicked(){
        presenter.doB();
    }
}

由于我們沒有LCE(Loading-Content-Error),所以不把?MvpLceActivity作為基類。我們采用的是最普遍的支持?ViewState的MvpViewStateActivity作為基類。基本上我們的View只顯示aView?或?bView。

onNew ViewStateInstance()中,我們需要明確在第一個Activity運行時需要做什么,因為先前并不存在ViewState?例子用于修復。在showA(A a)?和?showB(B b)中,我們需要將顯示A?或?B的信息存儲到ViewState。到這一步,我們就差不多完成了,現在只差MyCustomViewState執行這一步啦:

ublic class MyCustomViewState implements RestoreableViewState<MyCustomView>{

    private  final String  KEY_STATE="MyCustomViewState-flag";
    private  final String  KEY_DATA="MyCustomViewState-data";

    public boolean showingA=true;// if false, then show B
    public Parcelable  data;// Can be A or B

    @Override public void saveInstanceState(Bundle out){
        out.putBoolean (KEY_STATE,showingA);
        out.putParcelable (KEY_DATA,data);
    }

    @Override public boolean restoreInstanceState(Bundle in){
        if(in==null){
         return false;
        }

        showingA = in.getBoolean (KEY_STATE,true);
        data = in.getParcelable (KEY_DATA);
        return true;
    }

    @Override public void apply(MyCustomView view,boolean retained){

        if(showingA){
            view.showA((A)data);
        }else{
            view.showB((B)data);
        }
    }

    /**
       * @param a true if showing a, false if showing b
       */
    public void setShowingA(boolean a){
        this.showingA=a;
    }

    public void setData(Parcelable data){
        this.data=data;
    }
}

大家可以看到,我們需要把ViewState保存到從Activity.onSaveInstanceState()調用的?saveInstanceState()中,并且在從Activity.onCreate()調用的restoreInstanceState()中修復viewstate的數據。apply()方法將會從Activity中調用以修復view state。我們像presenter一樣通過調用同樣的View interface 方法showA()?或?showB()來實現這一操作。

大家可以看到,我們需要把ViewState保存到從Activity.onSaveInstanceState()調用的?saveInstanceState()中,并且在從Activity.onCreate()調用的restoreInstanceState()中修復viewstate的數據。apply()方法將會從Activity中調用以修復view state。我們像presenter一樣通過調用同樣的View interface 方法showA()?或?showB()來實現這一操作。

這個外部的ViewState把view state修復的復雜性和職責從Activity代碼中剝離,并入到這個單獨的類中。而編寫ViewState類的單元測試要比Activity類的單元測試容易得多。

怎樣處理后臺線程?

通常,Presenter會管理后臺線程。Presenter如何處理后臺線程取決于它所關聯的Activity或者Fragment ,具體分為兩種情況:

  • 可保持的Fragment : 如果你調用了Fragment的setRetainInstanceState(true)那么這個Fragment在屏幕旋轉時就不會被銷毀。只有該Fragment的GUI會被銷毀,并且在屏幕旋轉時重新調用onCreateView創建視圖。這就是說當屏幕旋轉時Fragment所有的成員成員變量和Presenter不會發生變化。在這個示例中,我們將新的視圖關聯到Presenter中。因此,Presenter不需要去掉任何正在運行中的后臺任務,因為Presenter已經關聯了新的視圖。例如:

1.豎屏情況下啟動應用 2.實例化Fragment時會調用onCreate()、onCreateView()、createPresenter(), 然后通過調用presenter的attachView()函數將View關聯到Presenter中。

  1. 下一步我們旋轉手機屏幕,從豎屏切換到橫屏;
  2. 此時onDestroyView()?會調用,而onDestroyView()?又會調用presenter的detachView(true)函數。我們注意到detachView有個參數為true,這是告訴presenter這個Fragment是可持有的Fragment(否則這個參數應該為false)。通過這個參數,presenter就知道它不需要取消正在運行的后臺任務;
  3. 應用現在是橫屏狀態了,在旋轉時onCreateView方法會被調用,但是createPresenter()函數不會被調用,因為我們會對presenter 進行不為空的判斷,當presenter為空時才調用createPresenter()函數。而Fragment的setRetainInstanceState(true)會保持這個presenter對象,因此presenter此時不會被重新創建;
  4. 在調用了presenter的attachView()之后新創建的View會被重新關聯到presenter中。
  5. ViewState會被恢復,但是沒有后臺任務會被取消,因此也沒有后臺任務需要重新啟動。
  • Activity和不保持的Fragment :在這個示例中工作流非常的簡單。所有的東西都會被銷毀,包括presenter。因此presenter對象應該取消所有正在運行的任務。例如 : 我們采用非保持fragment在豎屏情況下啟動app。

8.我們采用非保持fragment在豎屏情況下啟動app。 9.Fragment被實例化之后,調用onCreate(),?onCreateView(),和createPresenter(),然后通過調用presenter.attachView()view(fragment)附著到presenter。 10.下一步我們旋轉設備屏幕,從豎屏切換到橫屏。 11.此時onDestroyView()?會調用,而onDestroyView()?又會調用presenterdetachView(true)函數。Presenter取消后臺任務。

  1. onSaveInstanceState(Bundle)被調用,?ViewState被保存到Bundle中。
  2. App現在出于橫屏狀態。新的Fragment被實例化并調用onCreate(),onCreateView()和?createPresenter()來創建一個新的presenter例子,通過調用presenter.attachView()將新的view附著到新的presenter
  3. ViewState會從Bundle中恢復,且view的狀態也會被恢復。如果ViewState是showLoading,那么presenter會重新啟動后臺線程來加載數據。
  4. 以下是獲得ViewState支持的Activity的生命周期圖解,如圖1-6:

以下是獲得ViewState支持的Fragment的生命周期圖解, 如圖 1-7:

Retrofit模塊

Mosby提供了?LceRetrofitPresenter?和?LceCallback。為獲得LCE方法showLoading(),?showContent()?和?showError()支持的Retrofit編寫presenter ,幾行代碼就能搞定。

public class MembersPresenter extends LceRetrofitPresenter<MembersView,List<User>>{

    private  GithubApigithubApi;

    public MembersPresenter(GithubApi githubApi){
        this.githubApi=githubApi;
    }

    public void loadSquareMembers(boolean pullToRefresh){
        githubApi.getMembers("square",new LceCallback(pullToRefresh));
    }
}

Dagger模塊

想在不依靠注入式的情況下寫應用?Ted Mosby告訴你,這是行不通滴!Dagger是java依賴注入式框架最常用的方法,也是Android開發者們的心頭好。Mosby支持Dagger1。Mosby通過一個叫做getObjectGraph()的方法提供Injector界面。通常,我們的應用模塊非常廣泛。要想輕松分享這一模塊,我們需要把android.app.Application歸入子類,使其執行Injector。之后所有的Activity和Fragment都可以通過調用getObjectGraph()來存取ObjectGraph,因為DaggerActivity and DaggerFragment也都是Injector。我們也可以通過重寫Activity 或 Fragment中的?getObjcetGraph()?,從而調用plus(Module)以增加模塊。我個人已經用到Dagger2了,它與Mosby也兼容。大家可以在Github上找到關于Dagger1 和 Dagger2的示例。點此這個鏈接https://db.tt/3fVqVdAzDagger1示例 apk;點此這個鏈接https://db.tt/z85y4fSYDagger2 示例 apk。

Rx模塊

Observables贊爆了!現在稍微潮一點的小伙兒們都用RxJava了好嗎!你猜結果怎么著?RxJava確實是太酷了!所以,Mosby給大家提供一個本質上是Subscriber的MvpLceRxPresenter,它能幫我們自動處理onNext(),?onCompleted()?和?onError()并回調相應的LCE方法,比如showLoading(),?shwoContent()?和?showError()。它還將 RxAndroid 附帶到observerOn()?Android主要 UI 線程。你可能覺得,要是用了RxJava的話就不再需要Model View Presenter了。呃,那只是你的一家之言。在我看來,把View和Model清晰地區分開來非常重要。而且我也認為其中的某些好用的功能在沒有MVP的情況下不容易執行。最后,大家要是還想回到過去那個Activity和Fragment包含了上千條又臭又長的代碼行時代,那么我祝你在面條式代碼的地獄里過得愉快。好了,廢話不多說,我介紹的方法不屬于面條式代碼是因為Observerables引入了一個結構齊整的工作流,把Activity或Fragment做成一個BLOB的想法已經近在咫尺了。

測試模塊

大家可能注意到這里存在著一個測試模塊。這個模塊用于Mosby庫的內部測試。但是,它也可以為我們自己的app所用。它使用Robolectric為我們的LCE Presenter, Activities 和 Fragments提供單元測試模板。它的基本功能是查看測試中的Presenter是否正確工作:通過觀察presenter時候調用showLoading(), showContent()?和?showError()。我們還可以驗證setData()中的數據。所以我們可以為Presenter和底層編寫類似黑匣子的測試。Mosby的測試模塊也提供了測試MvpLceFragment?或?MvpLceActivity的可能性。它相當于一種“精簡版”的UI 測試。這些測試通過查看xml布局是否包含R.id.loadingView,?R.id.contentView?和R.id.errorView之類的指定id、loadingView是否可視,在加載view時,是否是錯誤的view可視、content view能否處理由setData()提交的已加載數據等方面來檢驗Fragment或Activity是否正常工作,是否遇到crashing。它和Espresso類的UI測試并不相同。我覺得沒有必要為LCE View單獨寫一個UI 測試。

以下是Ted Mosby庫的一些測試小建議:

  1. 編寫傳統的單元測試來測試業務邏輯層和model。
  2. 使用MvpLcePresenterTest來測試presenter。 3.使用MvpLceFragmentTest?和?MvpLceActivityTest來測試MvpLceFragment 和 Activity。 4.如果有必要,可以使用Espresso來編寫UI測試。

測試模塊尚未完成。大家可以看到這個模塊是測試版,因為Robolectric 3.0還沒完成,而且Android gradle plugin也沒用完全支持傳統的單元測試。android gradle plugin

1.2應該會好得多。Robolectric 和 androids gradle plugin可以用了之后我會再寫一篇關于Mosby,Dagger,Retrofit和RxJava單元測試的博客。


所屬標簽

無標簽

25选5玩法中奖