[譯]用MVI編寫響應式APP第二部分View和Intent

在第一部分咱們討論了關於什麼纔是真正的Model,Model和狀態的關係,而且討論了什麼樣的Model才能避免安卓開發過程當中的共性問題。在這篇咱們經過講Model-View-Intent模式去構建響應式安卓程序,繼續咱們的「響應式APP開發」探索之旅。java

若是你沒有閱讀第一部分,你應該先讀那篇而後再讀這篇。我在這裏先簡單的回顧一下上一部分的主要內容:咱們不要寫相似於下面的代碼(傳統的MVP的例子)android

class PersonsPresenter extends Presenter<PersonsView> {

  public void load(){
    getView().showLoading(true); // Displays a ProgressBar on the screen

    backend.loadPersons(new Callback(){
      public void onSuccess(List<Person> persons){
        getView().showPersons(persons); // Displays a list of Persons on the screen
      }

      public void onError(Throwable error){
        getView().showError(error); // Displays a error message on the screen
      }
    });
  }
}
複製代碼

咱們應該建立一個反應"狀態(State)"的"Model":git

class PersonsModel {
  // 在正式的項目裏應當爲私有
  // 咱們須要用get方法來獲取它們的值
  final boolean loading;
  final List<Person> persons;
  final Throwable error;

  public(boolean loading, List<Person> persons, Throwable error){
    this.loading = loading;
    this.persons = persons;
    this.error = error;
  }
}
複製代碼

而後Presenter的實現相似於下面這樣:github

class PersonsPresenter extends Presenter<PersonsView> {

  public void load(){
    getView().render( new PersonsModel(true, null, null) ); //顯示加載進度條

    backend.loadPersons(new Callback(){
      public void onSuccess(List<Person> persons){
        getView().render( new PersonsModel(false, persons, null) ); // 顯示人列表
      }

      public void onError(Throwable error){
          getView().render( new PersonsModel(false, null, error) ); // 顯示錯誤信息
      }
    });
  }
}
複製代碼

如今View有一個Model,經過調用render(personsModel) 方法,將數據渲染到UI上。在上一篇文章裏咱們也討論了單向數據流的重要性,而且你的業務邏輯應當驅動你的Model。在咱們把全部的內容連起來以前,咱們先快速的瞭解一下MVI的大意。編程

Model-View-Intent(MVI)

這個模式被 André Medeiros (Staltz) 爲了他寫的一個JavaScript的框架而提出的,這個框架的名字叫作 cycle.js 。從理論上(數學上)來看,咱們能夠用下面的表達式來描述Model-View-Intent:安全

  • intent() :這個函數接受用戶的輸入(例如,UI事件,像點擊事件之類的)並把它轉化成model函數的可接收的參數。這個參數多是一個簡單的String,也多是其餘複雜的結構的數據,像Object。咱們能夠說咱們經過intent()的意圖去改變Model。
  • model() :model()函數接收intent()函數的輸出做爲輸入,去操做Model。它的輸出是一個新的Model(由於狀態改變)。所以咱們不該該去更新已經存在的Model。由於咱們須要Model具備不變性! 在第一部分,我具體用」計數APP「做爲簡單的例子講了數據不變性的重要性。再次強調,咱們不要去修改已經存在的Model實例。咱們在model()方法裏建立新的,根據intent的輸出變化之後的Model。請注意,model()方法是你惟一可以建立新的Model對象的地方。基本上,咱們稱model()方法爲咱們App的業務邏輯(能夠是Interactor,Usecase,Repository ...您在應用中使用的任何模式/術語)而且傳遞新的Model對象做爲結果。
  • view() :這個方法接收model()方法的輸出值。而後根據model()的輸出值來渲染到UI上。view()方法大體上相似於view.render(model)

可是,咱們不是去構建一個」響應式的APP「,不是麼?因此,MVI是如何作到"響應式"的?"響應式"到底意味着什麼?先回答最後一個問題,」響應式「就是咱們的app根據狀態不一樣而去改變UI。在MVI中,」狀態「被"Model"所表明,實質上咱們指望,咱們的業務邏輯根據用戶的輸入事件(intent)產生新的"Model",而後再將新的"Model"經過調用view的render(Model)方法改變在UI。這就是MVI實現響應式的基本思路。網絡

使用RxJava來鏈接不一樣的點(這裏的點是指☞Model,View,Intent本來是相互獨立的點)

咱們想要讓咱們的數據流是單向的。RxJava在這裏起到了做用。咱們必須使用RxJava構建單向數據流的響應式App或MVI模式的App麼?不是的,咱們能夠用其餘的代碼實現。然而,RxJava對於事件基礎的編程是很好用的。既然用戶界面是基於事件的,使用RxJava也就頗有意義的。app

在這個系列博客,咱們將要開發一個簡單的電商應用。咱們在後臺進行http請求,去加載咱們須要顯示商品。咱們能夠搜索商品和添加商品到購物車。綜上所述整個App看起來想下面這個動圖:框架

這個項目的源代碼你能夠在 github 上找到。咱們先去實現一個簡單的頁面:實現搜索頁面。首先,咱們先定義一個最終將被View顯示的Model。 在這個系列博客咱們採用"ViewState"標示來標示Model ,例如:咱們的搜索頁面的Model類叫作 SearchViewState ,由於Model表明狀態(State)。至於爲何不使用SearchModel這樣的名字,是由於怕與MVVM的相似於SearchViewModel的命名混淆。命名真的很難。

public interface SearchViewState {

  /** *搜索尚未開始 */
  final class SearchNotStartedYet implements SearchViewState {
  }

  /** * 加載: 等待加載 */
  final class Loading implements SearchViewState {
  }

  /** *標識返回一個空結果 */
  final class EmptyResult implements SearchViewState {
    private final String searchQueryText;

    public EmptyResult(String searchQueryText) {
      this.searchQueryText = searchQueryText;
    }

    public String getSearchQueryText() {
      return searchQueryText;
    }
  }

  /** * 驗證搜索結果. 包含符合搜索條件的項目列表。 */
  final class SearchResult implements SearchViewState {
    private final String searchQueryText;
    private final List<Product> result;

    public SearchResult(String searchQueryText, List<Product> result) {
      this.searchQueryText = searchQueryText;
      this.result = result;
    }

    public String getSearchQueryText() {
      return searchQueryText;
    }

    public List<Product> getResult() {
      return result;
    }
  }

  /** *標識搜索出現的錯誤狀態 */
  final class Error implements SearchViewState {
    private final String searchQueryText;
    private final Throwable error;

    public Error(String searchQueryText, Throwable error) {
      this.searchQueryText = searchQueryText;
      this.error = error;
    }

    public String getSearchQueryText() {
      return searchQueryText;
    }

    public Throwable getError() {
      return error;
    }
  }
}
複製代碼

Java是個強類型的語言,咱們須要爲咱們的Model選擇一個安全的類型。咱們的業務邏輯返回的是 SearchViewState 類型的。固然這種定義方法是我我的的偏好。咱們也能夠經過不一樣的方式定義,例如:ide

class SearchViewState {
  Throwable error; // if not null, an error has occurred
  boolean loading; // if true loading data is in progress
  List<Product> result; // if not null this is the result of the search
  boolean SearchNotStartedYet; // if true, we have the search not started yet
}
複製代碼

再次強調,你能夠按照你的方式來定義你的Model。若是,你會使用kotlin語言的話,那麼sealed classes是一個很好的選擇。 下一步,讓我將聚焦點從新回到業務邏輯。讓咱們看一下負責執行搜索的 SearchInteractor 如何去實現。先前已經說過了它的"輸出"應該是一個 SearchViewState 對象。

public class SearchInteractor {
  final SearchEngine searchEngine; // 進行http請求

  public Observable<SearchViewState> search(String searchString) {
    // 空的字符串,因此沒搜索
    if (searchString.isEmpty()) {
      return Observable.just(new SearchViewState.SearchNotStartedYet());
    }

    // 搜索商品
    return searchEngine.searchFor(searchString) // Observable<List<Product>>
        .map(products -> {
          if (products.isEmpty()) {
            return new SearchViewState.EmptyResult(searchString);
          } else {
            return new SearchViewState.SearchResult(searchString, products);
          }
        })
        .startWith(new SearchViewState.Loading())
        .onErrorReturn(error -> new SearchViewState.Error(searchString, error));
  }
}
複製代碼

讓咱們看一下SearchInteractor.search()的方法簽名:咱們有一個字符串類型的searchString做爲輸入參數,和Observable 做爲輸出。這已經暗示咱們指望隨着時間的推移在這個可觀察的流上發射任意多個SearchViewState實例。startWith() 是在咱們開始查詢(經過http請求)以前調用的。咱們在startWith這裏發射SearchViewState.Loading 。目的是,當咱們點擊搜索按鈕,會有一個進度條出現。

onErrorReturn() 捕獲全部的在執行搜索的時候出現的異常,而且,發射一個SearchViewState.Error 。當咱們訂閱這個Observable的時候,咱們爲何不僅用onError的回調?這是對RxJava一個共性的誤解:onError回調意味着咱們整個觀察流進入了一個不可恢復的狀態,也就是整個觀察流已經被終止了。可是,在咱們這裏的錯誤,像無網絡之類的,不是不可恢復的錯誤。這僅僅是另外一種狀態(被Model表明)。此外,以後,咱們能夠移動到其餘狀態。例如,一旦咱們的網絡從新鏈接起來,那麼咱們能夠移動到被SearchViewState.Loading 表明的「加載狀態」。所以,咱們創建了一個從咱們的業務邏輯到View的觀察流,每次發射一個改變後的Model,咱們的"狀態"也會隨着改變。咱們確定不但願咱們的觀察流由於網絡錯誤而終止。所以,這類錯誤被處理爲一種被Model表明的狀態(除去那些致命錯誤)。一般狀況下,在MVI中可觀察對象Model不會被終止(永遠不會執行onComplete()或onError())。

對上面部分作個總結:SearchInteractor(業務邏輯)提供了一個觀察流Observable ,而且當每次狀態變化的時候,發射一個新的SearchViewState。

下一步,讓我討論View層長什麼樣子的。View層應該作什麼?顯然的,view應該去顯示Model。咱們已經贊成,View應當有一個像render(model) 這樣的方法。另外,View須要提供一個方法給其餘層用來接收用戶輸入的事件。這些事件在MVI中被稱做 intents 。在這個例子中,咱們僅僅只有一個intent:用戶能夠經過在輸入區輸入字符串來搜索。在MVP中一個好的作法是咱們能夠爲View定義接口,因此,在MVI中,咱們也能夠這樣作。

public interface SearchView {

  /** * The search intent * * @return An observable emitting the search query text */
  Observable<String> searchIntent();

  /** * Renders the View * * @param viewState The current viewState state that should be displayed */
  void render(SearchViewState viewState);
}
複製代碼

在這種狀況下,咱們的View僅僅提供一個intent,可是,在其餘業務狀況下,可能須要多個intent。在第一部分咱們討論了爲何單個render()方法(譯者:渲染方法)是一個好的方式,若是,你不清楚爲何咱們須要單個render(),你能夠先去閱讀第一部分。在咱們具體實現View層以前,咱們先看一下最後搜索頁面是什麼樣的

public class SearchFragment extends Fragment implements SearchView {

  @BindView(R.id.searchView) android.widget.SearchView searchView;
  @BindView(R.id.container) ViewGroup container;
  @BindView(R.id.loadingView) View loadingView;
  @BindView(R.id.errorView) TextView errorView;
  @BindView(R.id.recyclerView) RecyclerView recyclerView;
  @BindView(R.id.emptyView) View emptyView;
  private SearchAdapter adapter;

  @Override public Observable<String> searchIntent() {
    return RxSearchView.queryTextChanges(searchView) // Thanks Jake Wharton :)
        .filter(queryString -> queryString.length() > 3 || queryString.length() == 0)
        .debounce(500, TimeUnit.MILLISECONDS);
  }

  @Override public void render(SearchViewState viewState) {
    if (viewState instanceof SearchViewState.SearchNotStartedYet) {
      renderSearchNotStarted();
    } else if (viewState instanceof SearchViewState.Loading) {
      renderLoading();
    } else if (viewState instanceof SearchViewState.SearchResult) {
      renderResult(((SearchViewState.SearchResult) viewState).getResult());
    } else if (viewState instanceof SearchViewState.EmptyResult) {
      renderEmptyResult();
    } else if (viewState instanceof SearchViewState.Error) {
      renderError();
    } else {
      throw new IllegalArgumentException("Don't know how to render viewState " + viewState);
    }
  }

  private void renderResult(List<Product> result) {
    TransitionManager.beginDelayedTransition(container);
    recyclerView.setVisibility(View.VISIBLE);
    loadingView.setVisibility(View.GONE);
    emptyView.setVisibility(View.GONE);
    errorView.setVisibility(View.GONE);
    adapter.setProducts(result);
    adapter.notifyDataSetChanged();
  }

  private void renderSearchNotStarted() {
    TransitionManager.beginDelayedTransition(container);
    recyclerView.setVisibility(View.GONE);
    loadingView.setVisibility(View.GONE);
    errorView.setVisibility(View.GONE);
    emptyView.setVisibility(View.GONE);
  }

  private void renderLoading() {
    TransitionManager.beginDelayedTransition(container);
    recyclerView.setVisibility(View.GONE);
    loadingView.setVisibility(View.VISIBLE);
    errorView.setVisibility(View.GONE);
    emptyView.setVisibility(View.GONE);
  }

  private void renderError() {
    TransitionManager.beginDelayedTransition(container);
    recyclerView.setVisibility(View.GONE);
    loadingView.setVisibility(View.GONE);
    errorView.setVisibility(View.VISIBLE);
    emptyView.setVisibility(View.GONE);
  }

  private void renderEmptyResult() {
    TransitionManager.beginDelayedTransition(container);
    recyclerView.setVisibility(View.GONE);
    loadingView.setVisibility(View.GONE);
    errorView.setVisibility(View.GONE);
    emptyView.setVisibility(View.VISIBLE);
  }
}
複製代碼

render(SearchViewState) 這個方法,咱們經過看,就知道它是幹什麼的。在 searchIntent() 方法中咱們用到了Jake Wharton’s的RxBindings 庫,它使RxJava像綁定可觀察對象同樣綁定安卓UI控件。 RxSearchView.queryText()建立一個 Observable對象,每當用戶在EditText輸入的一些字符,發射須要搜索的字符串。咱們用filter()去保證只有當用戶輸入的字符數超過三個的時候,纔開始搜索。而且,咱們不但願每當用戶輸入一個新字符的時候就請求網絡,而是當用戶輸入完成之後再去請求網絡(debounce()停留500毫秒,決定用戶是否輸入完成)。

所以,咱們知道對於這個頁面而言,輸入是searchIntent(),輸出是render()。咱們如何從「輸入」到「輸出」?下面的視頻將這個過程可視化了:

其他的問題是誰或如何把咱們的View的意圖(intent)和業務邏輯聯繫起來?若是你已經看過了上面的視頻,能夠看到在中間有一個RxJava的操做符 flatMap() 。這暗示了咱們須要調用額外的組件,可是,咱們至今爲止尚未討論,它就是 Presenter 。Presenter將全部分離的不一樣點(譯者:這裏指Model,View,Intent這三個點)聯繫起來。它與MVP中的Presenter相似。

public class SearchPresenter extends MviBasePresenter<SearchView, SearchViewState> {
  private final SearchInteractor searchInteractor;

  @Override protected void bindIntents() {
    Observable<SearchViewState> search =
        intent(SearchView::searchIntent)
            .switchMap(searchInteractor::search) // 我在上面視頻中用flatMap()可是 switchMap() 在這裏更加適用
            .observeOn(AndroidSchedulers.mainThread());

    subscribeViewState(search, SearchView::render);
  }
}
複製代碼

MviBasePresenter 是什麼?這個是我寫的一個庫叫 Mosby (Mosby3.0已經添加了MVI組件)。這篇博客不是爲介紹Mosby而寫的,可是,我想對MviBasePresenter作個簡短的介紹。介紹一下MviBasePresenter如何讓你方便使用的。這個庫裏面沒有什麼黑魔法。讓咱們從lifecycle(生命週期)開始說:MviBasePresenter事實上沒有lifecyle(生命週期)。有一個 bindIntent() 方法將視圖的意圖(intent)與業務邏輯綁定。一般,你用flatMap()或switchMap 亦或concatMap(),將意圖(intent)傳遞給業務邏輯。這個方法的調用僅僅在View第一次被附加到Presenter。當View從新附加到Presenter時,將不會被調用(例如,當屏幕方向改變)。

這聽起來很奇怪,也許有人會說:「MviBasePresenter在屏幕方向變化的時候都能保持?若是是的話,Mosby是如何確保可觀察流的數據在內存中,而不被丟失?」,這是intent()subscribeViewState() 的就是用來回答這個問題的。intent() 在內部建立一個PublishSubject ,並將其用做你的業務邏輯的「門戶」。因此實際上這個PublishSubject訂閱了View的意圖(intent)可觀察對象( Observable)。調用intent(o1)實際上返回一個訂閱了o1的PublishSubject。

當方向改變的時候,Mosby從Presenter分離View,可是,僅僅只是暫時的取消訂閱內部的PublishSubject。而且,當View從新鏈接到Presenter的時候,將PublishSubject從新訂閱View的意圖(intent)。

subscribeViewState() 用不一樣的方式作的是一樣的事情(Presenter到View的通訊)。它在內部建立一個BehaviorSubject 做爲業務邏輯到View的「門戶」。既然是BahaviorSubject,咱們能夠從業務邏輯收到「模型更新」的信息,即便是目前沒有view附加(例如,View正處於返回棧)。BehaviorSubjects老是保留最後時刻的值,每當有View附加到上面的時候,它就開始從新接收,或者將它保留的值傳遞給View。

規則很簡單:用intent()去「包裝」全部View的意圖(點擊事件等)。用subscribeViewState()而不是Observable.subscribe(...)。

和bindIntent()對應的是unbindIntents() ,這兩個方法僅僅會被調用一次,當unbindIntents()調用的時候,那麼View就會被永久銷燬。舉個例子,將fragment處於返回棧,不去永久銷燬view,可是若是一個Activity結束了它的生命週期,就會永久銷燬view。因爲intent()和subscribeViewState()已經負責訂閱管理,因此你幾乎不須要實現unbindIntents()。

那麼關於咱們生命週期中的onPause()onResume() 是如何處理的?我認爲Presenters是不須要關注生命週期 。若是,你非要在Presenter中處理生命週期,好比你將onPause()做爲intent。你的View須要提供一個pauseIntent() 方法,這個方法是由生命週期觸發的,而不是用戶交互觸發的,但二者都是有效的意圖。

總結

在第二部分,咱們討論了關於Model-View-Intent的基礎,而且用MVI實現了一個簡單的搜索頁面。讓咱們入門。也許這個例子太簡單了。你沒法看出MVI的優點,Model表明狀態和單向數據流一樣適用於傳統的MVP或MVVM。MVP和MVVM都很優秀。MVI也許並無它們優秀。即便如此,我認爲MVI幫助咱們面對複雜問題的時候寫優雅的代碼。咱們將在這個系列博客第三部分,討論狀態減小。

相關文章
相關標籤/搜索