Mosby MVP使用教程

目錄

1.MVP簡(jiǎn)介
2.Mosby簡(jiǎn)介
3.Hello MVP World
4.MvpPresenter的基類(lèi)
5.LCE視圖
6.MvpLceActivity和MvpLceFragment
7.ViewState簡(jiǎn)介
8.思考

一.MVP簡(jiǎn)介

MVP的出發(fā)點(diǎn)是關(guān)注點(diǎn)分離,將視圖和業(yè)務(wù)邏輯解耦。Model-View-Presenter三個(gè)部分可以簡(jiǎn)單理解為:

  • Model是將在視圖中顯示的數(shù)據(jù)。
  • View是顯示數(shù)據(jù)(model)的界面,同時(shí)將用戶(hù)指令(事件)發(fā)送給Presenter來(lái)處理。View通常含有Presenter的引用。在Android中Activity,F(xiàn)ragment和ViewGroup都扮演視圖的角色。
  • Presenter是中間人,同時(shí)有兩者的引用。請(qǐng)注意單詞model非常有誤導(dǎo)性。它應(yīng)該是獲取或處理model的業(yè)務(wù)邏輯。例如:如果你的數(shù)據(jù)庫(kù)表中存儲(chǔ)著User,而你的視圖想顯示用戶(hù)列表,那么Presenter將有一個(gè)數(shù)據(jù)庫(kù)業(yè)務(wù)邏輯(例如DAO)類(lèi)的引用,Presenter通過(guò)它來(lái)查詢(xún)用戶(hù)列表。

思考:MVC,MVP和MVVM之間有什么區(qū)別和聯(lián)系?

  • 消極視圖:在MVP中,View是消極視圖(Passive View),也就是說(shuō)它盡量不去主動(dòng)做事,而是讓Presenter通過(guò)抽象方式控制View,例如Presenter調(diào)用view.showLoading()方法來(lái)顯示加載效果,但Presenter不應(yīng)該控制View的具體實(shí)現(xiàn),例如動(dòng)畫(huà),所以Presenter不應(yīng)該調(diào)用view.startAnimation()這樣的方法。
MVP

二.Mosby簡(jiǎn)介

設(shè)計(jì)目標(biāo):讓你能用清晰的Model-View-Presenter架構(gòu)來(lái)構(gòu)建Android app。

注意:Mosby是一個(gè)庫(kù)(library),不是一個(gè)框架(framework)。

思考:什么是library?什么是framework?它們的區(qū)別是什么?
Mosby的內(nèi)核是一個(gè)基于委托模式(delegation)的很精簡(jiǎn)的庫(kù)。你可以使用委托(delegation)和組合(composition)將Mosby集成到你的開(kāi)發(fā)技術(shù)棧中。這樣你就能避免框架(framework)帶來(lái)的限制和約束。

思考:什么是委托模式?委托和繼承的區(qū)別是什么?使用委托有什么好處?
依賴(lài):

  • Mosby被分成模塊,你可以選擇你需要的功能:

    dependencies {
        compile 'com.hannesdorfmann.mosby:mvp:2.0.1'
        compile 'com.hannesdorfmann.mosby:viewstate:2.0.1'
    }
    

三.Hello MVP World

先來(lái)用Mosby MVP庫(kù)來(lái)實(shí)現(xiàn)一個(gè)最簡(jiǎn)單的功能,頁(yè)面有兩個(gè)Button和一個(gè)TextView,需求如下:

  • 點(diǎn)擊Hello按鈕,顯示紅色文本 "Hello" + 隨機(jī)數(shù);
  • 點(diǎn)擊Goodbye按鈕,顯示藍(lán)色文本 "Goodbye" + 隨機(jī)數(shù);
    這里假設(shè)隨機(jī)數(shù)的生成過(guò)程涉及到復(fù)雜的業(yè)務(wù)邏輯計(jì)算,是一個(gè)耗時(shí)操作,需要2s時(shí)間。

<b>第一步</b>我們用一個(gè)AsyncTask來(lái)實(shí)現(xiàn)這個(gè)模擬的業(yè)務(wù)邏輯,在自定義的AsyncTask中,要定義一個(gè)監(jiān)聽(tīng)器,用來(lái)傳遞業(yè)務(wù)邏輯執(zhí)行結(jié)果:

public class GreetingGeneratorTask extends AsyncTask<Void, Void, Integer>{

    // Callback - listener
    public interface GreetingTaskListener{
        void onGreetingGenerated(String greetingText);
    }

......

    // 模擬計(jì)算過(guò)程,返回一個(gè)隨機(jī)值。
    @Override
    protected Integer doInBackground(Void... params) {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return (int)(Math.random() * 100);
    }

    @Override
    protected void onPostExecute(Integer randomInt) {
        listener.onGreetingGenerated(baseText + " " + randomInt);
    }
}

<b>第二步</b>定義視圖接口,視圖接口需要繼承MvpView:

public interface HelloWorldView extends MvpView{

    void showHello(String greetingText);

    void showGoodbye(String greetingText);
}

注意這里的MvpView是所有視圖的頂層接口,它是一個(gè)空接口,沒(méi)有定義任何方法。

<b>第三步</b>實(shí)現(xiàn)Presenter,Presenter需要執(zhí)行業(yè)務(wù)邏輯,并針對(duì)不同的執(zhí)行結(jié)果調(diào)用視圖的對(duì)應(yīng)方法。

Presenter的頂層接口是MvpPresenter,它有兩個(gè)方法:

public interface MvpPresenter<V extends MvpView> {

  /**
   * 將View附著到Presenter上
   */
  public void attachView(V view);

  /**
   * 在視圖被摧毀時(shí)調(diào)用。典型場(chǎng)景是Activity.onDestroy()和  Fragment.onDestroyView()方法
   */
  public void detachView(boolean retainInstance);
}

Mosby提供了MvpPresenter接口的基類(lèi)實(shí)現(xiàn),在這里我們繼承MvpBasePresenter:

public class HelloWorldPresenter extends MvpBasePresenter<HelloWorldView>{

    private GreetingGeneratorTask greetingTask;

    private void cancelGreetingTaskIfRunning(){
        if (greetingTask != null){
            greetingTask.cancel(true);
        }    
    }

    public void greetHello(){
        cancelGreetingTaskIfRunning();

        greetingTask = new GreetingGeneratorTask("Hello", new     GreetingGeneratorTask.GreetingTaskListener() {
            @Override
           public void onGreetingGenerated(String greetingText) {
                if (isViewAttached()){
                    getView().showHello(greetingText);
                }
            }
        });
        greetingTask.execute();
    }    

......

    @Override
    public void detachView(boolean retainInstance) {
        super.detachView(retainInstance);
        if (!retainInstance){
            cancelGreetingTaskIfRunning();
        }
    }
}

注意在detachView方法中取消后臺(tái)任務(wù)的處理。

<b>第四步</b>實(shí)現(xiàn)Activity,讓我們的Activity繼承MvpActivity,并實(shí)現(xiàn)HelloWorldView接口。

MvpActivity有兩個(gè)泛型,分別是Presenter和View的具體類(lèi)型:

public class HelloWorldActivity extends MvpActivity<HelloWorldView, HelloWorldPresenter> implements HelloWorldView{
    ......
}

繼承MvpActivity后,只有一個(gè)抽象方法createPresenter()需要實(shí)現(xiàn):

public HelloWorldPresenter createPresenter() {
    return new HelloWorldPresenter();
}

HelloWorldView還有兩個(gè)方法需要實(shí)現(xiàn):

@Override
public void showHello(String greetingText) {
    greetingTextView.setTextColor(Color.RED);
    greetingTextView.setText(greetingText);
}

@Override
public void showGoodbye(String greetingText) {
    greetingTextView.setTextColor(Color.BLUE);
    greetingTextView.setText(greetingText);
}

點(diǎn)擊按鈕后,使用Presenter來(lái)完成相關(guān)操作:

@OnClick(R.id.helloButton)
public void onHelloButtonClicked(){
    presenter.greetHello();
}

@OnClick(R.id.goodbyeButton)
public void onGoodbyeButtonClicked(){
    presenter.greetGoodbye();
}

四.MvpPresenter的基類(lèi)

Presenter默認(rèn)實(shí)現(xiàn)一:使用弱引用保存視圖引用,在調(diào)用getView()之前必須判斷isViewAttached()。

public class MvpBasePresenter<V extends MvpView> implements MvpPresenter<V> {

  private WeakReference<V> viewRef;

  @Override public void attachView(V view) {
    viewRef = new WeakReference<V>(view);
  }

  @Nullable public V getView() {
    return viewRef == null ? null : viewRef.get();
  }

  public boolean isViewAttached() {
    return viewRef != null && viewRef.get() != null;
  }

  @Override public void detachView(boolean retainInstance) {
    if (viewRef != null) {
      viewRef.clear();
      viewRef = null;
    }
  }
}

Presenter默認(rèn)實(shí)現(xiàn)二:使用Null Object Pattern,在調(diào)用getView()時(shí)無(wú)需判斷。

public class MvpNullObjectBasePresenter<V extends MvpView> implements MvpPresenter<V> {

  private V view;

  @Override public void attachView(V view) {
    this.view = view;
  }

  @NonNull public V getView() {
    if (view == null) {
  throw new NullPointerException("MvpView reference is null. Have you called attachView()?");
    }
    return view;
  }

  @Override public void detachView(boolean retainInstance) {
    if (view != null) {
      Type[] types =
          ((ParameterizedType)     getClass().getGenericSuperclass()).getActualTypeArguments();

      Class<V> viewClass = (Class<V>) types[0];
      view = NoOp.of(viewClass);
    }
   }
}

思考:什么是空對(duì)象模式(Null Object Pattern)?

五.LCE視圖

  • 在開(kāi)發(fā)Android應(yīng)用過(guò)程中,我們會(huì)發(fā)現(xiàn)很多頁(yè)面有相似的結(jié)構(gòu)和UI邏輯,所以我們常常在寫(xiě)重復(fù)代碼。如果能抽象出相似頁(yè)面的View接口,然后封裝頁(yè)面的基類(lèi),就能讓開(kāi)發(fā)方便很多。Mosby就給我們提供了一個(gè)這樣的視圖模板,叫做LCE View。

  • LCE代表Loading-Content-Error(加載-內(nèi)容-錯(cuò)誤),此視圖有三種狀態(tài):顯示加載中,顯示數(shù)據(jù)內(nèi)容,或者顯示錯(cuò)誤視圖。例如在如下的場(chǎng)景中:

  • 假設(shè)我們要在ListView中顯示一個(gè)國(guó)家列表,國(guó)家列表的數(shù)據(jù)是從網(wǎng)絡(luò)獲取的,是一個(gè)耗時(shí)操作。在加載過(guò)程中,我們要顯示一個(gè)ProgressBar,如果加載出錯(cuò),我們要顯示一條錯(cuò)誤信息。另外,還要用SwipeRefreshLayout來(lái)讓用戶(hù)可以下拉刷新。

LCE View的接口定義如下:

public interface MvpLceView<M> extends MvpView {

  /**
   * 顯示加載視圖,加載視圖的id必須為R.id.loadingView
   */
  public void showLoading(boolean pullToRefresh);

  /**
   * 顯示內(nèi)容視圖,內(nèi)容視圖的id必須為R.id.contentView
   *
   * <b>The content view must have the id = R.id.contentView</b>
   */
  public void showContent();

  /**
   * 顯示錯(cuò)誤視圖,錯(cuò)誤視圖必須是TextView,id必須是R.id.errorView
   */
  public void showError(Throwable e, boolean pullToRefresh);

  /**
   * 設(shè)置將在showContent()中顯示的數(shù)據(jù)
   */
  public void setData(M data);

  /**
   * 加載數(shù)據(jù),此方法中常需要調(diào)用Presenter的對(duì)應(yīng)方法。因此此方法不可在Presenter
   * 中使用,避免循環(huán)調(diào)用。
   * 參數(shù)pullToRefresh代表此次加載是否由下拉刷新觸發(fā)。
   */
  public void loadData(boolean pullToRefresh);
}

思考:LCE視圖中考慮了下拉刷新,但沒(méi)有考慮上拉加載,如果服務(wù)器是分頁(yè)接口,需要添加上拉加載,應(yīng)該怎樣定義視圖接口?

六.MvpLceActivity和MvpLceFragment

Mosby封裝了LCE視圖的基類(lèi),現(xiàn)在我們用MvpLceActivity或MvpLceFragment來(lái)實(shí)現(xiàn)上面所說(shuō)的加載國(guó)家列表的場(chǎng)景。

<b>第一步:</b>完成界面布局,注意id必須使用上面指定的名稱(chēng),錯(cuò)誤視圖只能是一個(gè)TextView:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
......>
    <include layout="@layout/loading_view" />
    <include layout="@layout/error_view" />
    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/contentView"
        ......>
        <ListView ....../>
    </android.support.v4.widget.SwipeRefreshLayout>
</FrameLayout>

<b>第二步:</b>繼承MvpLceView實(shí)現(xiàn)自己的視圖接口,此處需要指定泛型,作為數(shù)據(jù)類(lèi)型:

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

<b>第三步:</b>實(shí)現(xiàn)Presenter,在這里我們做了一個(gè)接口和一個(gè)實(shí)現(xiàn):

接口定義:

public interface CountriesPresenter extends MvpPresenter<CountriesView>{

    void loadCountries(final boolean pullToRefresh);
}

具體實(shí)現(xiàn):

public class SimpleCountriesPresenter extends     MvpNullObjectBasePresenter<CountriesView>
        implements CountriesPresenter{

......

    @Override
    public void loadCountries(final boolean pullToRefresh) {
        getView().showLoading(pullToRefresh);

    ......

        countriesLoader = new CountriesAsyncLoader(++failingCounter % 2 != 0, new     CountriesAsyncLoader.CountriesLoaderListener() {
            @Override
            public void onSuccess(List<Country> countries) {
                getView().setData(countries);
                getView().showContent();
            }

            @Override
             public void onError(Exception e) {
                getView().showError(e, pullToRefresh);
            }    
        });

        countriesLoader.execute();
    }

......
}

上面代碼中就使用到了MvpLceView中除loadData()外的全部四個(gè)方法。

<b>第四步:</b>實(shí)現(xiàn)Activity或者Fragment,先以Activity為例,需要繼承MvpLceActivity:

public class CountriesActivity extends MvpLceActivity<SwipeRefreshLayout,     List<Country>, CountriesView, CountriesPresenter>
        implements SwipeRefreshLayout.OnRefreshListener, CountriesView{}

MvpLceActivity中定義了四個(gè)泛型,分別是ContentView的類(lèi)型,Data的類(lèi)型,視圖接口的類(lèi)型和Presenter的類(lèi)型。此處ContentView使用的是SwipeRefreshLayout。

繼承MvpLceActivity后有兩個(gè)方法需要實(shí)現(xiàn):

@Override
protected String getErrorMessage(Throwable e, boolean pullToRefresh) {
    if (pullToRefresh) {
        return "Error while loading countries";
    } else {
        return "Error while loading countries. Click here to retry";
    }
}

@NonNull
@Override
public CountriesPresenter createPresenter() {
    return new SimpleCountriesPresenter();
}

實(shí)現(xiàn)CountriesView接口后,重寫(xiě)如下幾個(gè)方法:

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

@Override
public void showContent() {
    super.showContent();
    contentView.setRefreshing(false);
}

@Override
public void showError(Throwable e, boolean pullToRefresh) {
    super.showError(e, pullToRefresh);
    contentView.setRefreshing(false);
}

@Override
public void loadData(boolean pullToRefresh) {
    presenter.loadCountries(pullToRefresh);
}

實(shí)現(xiàn)OnRefreshListener接口后,需要實(shí)現(xiàn)一個(gè)方法:

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

如果要用Fragment,方法基本上一樣,只需繼承MvpLceFragment,唯一的區(qū)別是,Activity的初始化在onCreate()中完成,F(xiàn)ragment的初始化在onViewCreated()中完成:

在Activity中:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.countries_list);
    ButterKnife.bind(this);

   contentView.setOnRefreshListener(this);
    adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1);
    listView.setAdapter(adapter);
    loadData(false);
}

在Fragment中:

@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    ButterKnife.bind(this, view);

    contentView.setOnRefreshListener(this);

    contentView.setOnRefreshListener(this);
    adapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_list_item_1);
    listView.setAdapter(adapter);
    loadData(false);
}

七.ViewState簡(jiǎn)介

  • 在Android開(kāi)發(fā)中有一個(gè)很麻煩的問(wèn)題,就是在界面被銷(xiāo)毀、被重建的過(guò)程中保存和恢復(fù)視圖狀態(tài)。界面被系統(tǒng)回收和重建常常發(fā)生在這兩個(gè)場(chǎng)景中:

  • Configuration變化,例如屏幕在橫豎屏之間切換,語(yǔ)言環(huán)境變化等。
    界面切到后臺(tái)(例如用戶(hù)按Home鍵),Android在內(nèi)存過(guò)低時(shí)自動(dòng)回收此Activity,在界面重新顯示時(shí)重建Activity。

思考:兩種Activity被回收和重建的場(chǎng)景,有什么區(qū)別?
Mosby提供了一個(gè)ViewState特性來(lái)解決這一問(wèn)題。ViewState是一個(gè)接口,只有一個(gè)apply方法:

public interface ViewState<V extends MvpView> {

  /**
   * Called to apply this viewstate on a given view.
   *
   * @param view The {@link MvpView}
   * @param retained true, if the components like the viewstate and the presenter have been
   * retained
   * because the {@link Fragment#setRetainInstance(boolean)} has been set to true
   */
  public void apply(V view, boolean retained);
}

例如,上面講過(guò)的MvpLceFragment,如果想在橫豎屏切換過(guò)程中保存和恢復(fù)視圖狀態(tài),只需改成繼承MvpLceViewStateFragment,實(shí)現(xiàn)如下一個(gè)方法即可:

    @Override
    public LceViewState<List<Country>, CountriesView> createViewState() {
        setRetainInstance(true);
        return new RetainingLceViewState<>();
    }

這是針對(duì)Mosby提供的LceView的ViewState,如果是我們的自定義視圖,也可以實(shí)現(xiàn)自己的ViewState。整個(gè)ViewState特性的實(shí)現(xiàn)原理和應(yīng)用方法比較復(fù)雜,這里不做過(guò)多介紹。

八.思考

  • 1.MVC,MVP和MVVM之間有什么區(qū)別和聯(lián)系?

  • 2.什么是委托模式?委托和繼承的區(qū)別是什么?使用委托有什么好處?

  • 3.什么是library?什么是framework?它們的區(qū)別是什么?

提示:library和framework的關(guān)鍵區(qū)別是“控制反轉(zhuǎn)”(Inversion of Control)。當(dāng)你調(diào)用library中的方法時(shí),你掌握控制權(quán)。但使用framework時(shí),控制是倒轉(zhuǎn)的:由framework來(lái)調(diào)用你的代碼。

  • 4.什么是空對(duì)象模式(Null Object Pattern)?

  • 5.LCE視圖中考慮了下拉刷新,但沒(méi)有考慮上拉加載,如果服務(wù)器是分頁(yè)接口,需要添加上拉加載,應(yīng)該怎樣定義視圖接口?

  • 6.兩種Activity被回收和重建的場(chǎng)景,有什么區(qū)別?

提示:參考下面兩個(gè)方法:

Fragmemt.setRetainInstance(boolean retain)
Activity.onRetainNonConfigurationInstance()
最后編輯于
?著作權(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)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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