[譯]用MVI編寫(xiě)響應(yīng)式APP第二部分View和Intent

在第一部分我們討論了關(guān)于什么才是真正的Model截歉,Model和狀態(tài)的關(guān)系厢绝,并且討論了什么樣的Model才能避免安卓開(kāi)發(fā)過(guò)程中的共性問(wèn)題炸庞。在這篇我們通過(guò)講Model-View-Intent模式去構(gòu)建響應(yīng)式安卓程序最欠,繼續(xù)我們的“響應(yīng)式APP開(kāi)發(fā)”探索之旅。

如果你沒(méi)有閱讀第一部分积担,你應(yīng)該先讀那篇然后再讀這篇陨晶。我在這里先簡(jiǎn)單的回顧一下上一部分的主要內(nèi)容:我們不要寫(xiě)類似于下面的代碼(傳統(tǒng)的MVP的例子)

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

我們應(yīng)該創(chuàng)建一個(gè)反應(yīng)"狀態(tài)(State)"的"Model":

class PersonsModel {
  // 在正式的項(xiàng)目里應(yīng)當(dāng)為私有
  // 我們需要用get方法來(lái)獲取它們的值
  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的實(shí)現(xiàn)類似于下面這樣:

class PersonsPresenter extends Presenter<PersonsView> {

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

    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) ); // 顯示錯(cuò)誤信息
      }
    });
  }
}

現(xiàn)在View有一個(gè)Model,通過(guò)調(diào)用render(personsModel) 方法,將數(shù)據(jù)渲染到UI上。在上一篇文章里我們也討論了單向數(shù)據(jù)流的重要性攻谁,并且你的業(yè)務(wù)邏輯應(yīng)當(dāng)驅(qū)動(dòng)你的Model采幌。在我們把所有的內(nèi)容連起來(lái)之前末早,我們先快速的了解一下MVI的大意。

Model-View-Intent(MVI)

這個(gè)模式被 André Medeiros (Staltz) 為了他寫(xiě)的一個(gè)JavaScript的框架而提出的,這個(gè)框架的名字叫做 cycle.js 。從理論上(數(shù)學(xué)上)來(lái)看铃芦,我們可以用下面的表達(dá)式來(lái)描述Model-View-Intent:

  • intent() :這個(gè)函數(shù)接受用戶的輸入(例如雅镊,UI事件,像點(diǎn)擊事件之類的)并把它轉(zhuǎn)化成model函數(shù)的可接收的參數(shù)刃滓。這個(gè)參數(shù)可能是一個(gè)簡(jiǎn)單的String仁烹,也可能是其他復(fù)雜的結(jié)構(gòu)的數(shù)據(jù),像Object咧虎。我們可以說(shuō)我們通過(guò)intent()的意圖去改變Model卓缰。
  • model() :model()函數(shù)接收intent()函數(shù)的輸出作為輸入,去操作Model砰诵。它的輸出是一個(gè)新的Model(因?yàn)闋顟B(tài)改變)征唬。因此我們不應(yīng)該去更新已經(jīng)存在的Model。因?yàn)槲覀冃枰狹odel具有不變性! 在第一部分茁彭,我具體用”計(jì)數(shù)APP“作為簡(jiǎn)單的例子講了數(shù)據(jù)不變性的重要性总寒。再次強(qiáng)調(diào),我們不要去修改已經(jīng)存在的Model實(shí)例尉间。我們?cè)趍odel()方法里創(chuàng)建新的偿乖,根據(jù)intent的輸出變化以后的Model。請(qǐng)注意哲嘲,model()方法是你唯一能夠創(chuàng)建新的Model對(duì)象的地方∠苯基本上眠副,我們稱model()方法為我們App的業(yè)務(wù)邏輯(可以是Interactor,Usecase竣稽,Repository ...您在應(yīng)用中使用的任何模式/術(shù)語(yǔ))并且傳遞新的Model對(duì)象作為結(jié)果囱怕。
  • view() :這個(gè)方法接收model()方法的輸出值。然后根據(jù)model()的輸出值來(lái)渲染到UI上毫别。view()方法大致上類似于view.render(model) 娃弓。

但是,我們不是去構(gòu)建一個(gè)”響應(yīng)式的APP“岛宦,不是么?所以台丛,MVI是如何做到"響應(yīng)式"的?"響應(yīng)式"到底意味著什么?先回答最后一個(gè)問(wèn)題砾肺,”響應(yīng)式“就是我們的app根據(jù)狀態(tài)不同而去改變UI挽霉。在MVI中,”狀態(tài)“被"Model"所代表变汪,實(shí)質(zhì)上我們期望侠坎,我們的業(yè)務(wù)邏輯根據(jù)用戶的輸入事件(intent)產(chǎn)生新的"Model",然后再將新的"Model"通過(guò)調(diào)用view的render(Model)方法改變?cè)赨I裙盾。這就是MVI實(shí)現(xiàn)響應(yīng)式的基本思路实胸。

使用RxJava來(lái)連接不同的點(diǎn)(這里的點(diǎn)是指?Model,View,Intent原本是相互獨(dú)立的點(diǎn))

我們想要讓我們的數(shù)據(jù)流是單向的他嫡。RxJava在這里起到了作用。我們必須使用RxJava構(gòu)建單向數(shù)據(jù)流的響應(yīng)式App或MVI模式的App么庐完?不是的钢属,我們可以用其他的代碼實(shí)現(xiàn)。然而假褪,RxJava對(duì)于事件基礎(chǔ)的編程是很好用的署咽。既然用戶界面是基于事件的,使用RxJava也就很有意義的生音。

在這個(gè)系列博客宁否,我們將要開(kāi)發(fā)一個(gè)簡(jiǎn)單的電商應(yīng)用。我們?cè)诤笈_(tái)進(jìn)行http請(qǐng)求缀遍,去加載我們需要顯示商品慕匠。我們可以搜索商品和添加商品到購(gòu)物車。綜上所述整個(gè)App看起來(lái)想下面這個(gè)動(dòng)圖:

這個(gè)項(xiàng)目的源代碼你可以在 github 上找到域醇。我們先去實(shí)現(xiàn)一個(gè)簡(jiǎn)單的頁(yè)面:實(shí)現(xiàn)搜索頁(yè)面台谊。首先,我們先定義一個(gè)最終將被View顯示的Model譬挚。在這個(gè)系列博客我們采用"ViewState"標(biāo)示來(lái)標(biāo)示Model ,例如:我們的搜索頁(yè)面的Model類叫做SearchViewState ,因?yàn)镸odel代表狀態(tài)(State)锅铅。至于為什么不使用SearchModel這樣的名字,是因?yàn)榕屡cMVVM的類似于SearchViewModel的命名混淆减宣。命名真的很難盐须。

public interface SearchViewState {

  /**
   *搜索還沒(méi)有開(kāi)始
   */
  final class SearchNotStartedYet implements SearchViewState {
  }

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

  /**
   *標(biāo)識(shí)返回一個(gè)空結(jié)果
   */
  final class EmptyResult implements SearchViewState {
    private final String searchQueryText;

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

    public String getSearchQueryText() {
      return searchQueryText;
    }
  }

  /**
   * 驗(yàn)證搜索結(jié)果. 包含符合搜索條件的項(xiàng)目列表。
   */
  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;
    }
  }

  /**
   *標(biāo)識(shí)搜索出現(xiàn)的錯(cuò)誤狀態(tài)
   */
  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是個(gè)強(qiáng)類型的語(yǔ)言漆腌,我們需要為我們的Model選擇一個(gè)安全的類型贼邓。我們的業(yè)務(wù)邏輯返回的是 SearchViewState 類型的。當(dāng)然這種定義方法是我個(gè)人的偏好闷尿。我們也可以通過(guò)不同的方式定義塑径,例如:

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
}

再次強(qiáng)調(diào),你可以按照你的方式來(lái)定義你的Model填具。如果统舀,你會(huì)使用kotlin語(yǔ)言的話,那么sealed classes是一個(gè)很好的選擇灌旧。

下一步绑咱,讓我將聚焦點(diǎn)重新回到業(yè)務(wù)邏輯。讓我們看一下負(fù)責(zé)執(zhí)行搜索的 SearchInteractor 如何去實(shí)現(xiàn)枢泰。先前已經(jīng)說(shuō)過(guò)了它的"輸出"應(yīng)該是一個(gè) SearchViewState 對(duì)象描融。

public class SearchInteractor {
  final SearchEngine searchEngine; // 進(jìn)行http請(qǐng)求

  public Observable<SearchViewState> search(String searchString) {
    // 空的字符串,所以沒(méi)搜索
    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()的方法簽名:我們有一個(gè)字符串類型的searchString作為輸入?yún)?shù)衡蚂,和Observable<SearchViewState> 作為輸出窿克。這已經(jīng)暗示我們期望隨著時(shí)間的推移在這個(gè)可觀察的流上發(fā)射任意多個(gè)SearchViewState實(shí)例骏庸。startWith() 是在我們開(kāi)始查詢(通過(guò)http請(qǐng)求)之前調(diào)用的。我們?cè)趕tartWith這里發(fā)射SearchViewState.Loading 年叮。目的是具被,當(dāng)我們點(diǎn)擊搜索按鈕,會(huì)有一個(gè)進(jìn)度條出現(xiàn)只损。

onErrorReturn() 捕獲所有的在執(zhí)行搜索的時(shí)候出現(xiàn)的異常一姿,并且,發(fā)射一個(gè)SearchViewState.Error 跃惫。當(dāng)我們訂閱這個(gè)Observable的時(shí)候叮叹,我們?yōu)槭裁床恢挥胦nError的回調(diào)?這是對(duì)RxJava一個(gè)共性的誤解:onError回調(diào)意味著我們整個(gè)觀察流進(jìn)入了一個(gè)不可恢復(fù)的狀態(tài)爆存,也就是整個(gè)觀察流已經(jīng)被終止了蛉顽。但是,在我們這里的錯(cuò)誤先较,像無(wú)網(wǎng)絡(luò)之類的携冤,不是不可恢復(fù)的錯(cuò)誤。這僅僅是另一種狀態(tài)(被Model代表)闲勺。此外曾棕,之后,我們可以移動(dòng)到其他狀態(tài)菜循。例如睁蕾,一旦我們的網(wǎng)絡(luò)重新連接起來(lái),那么我們可以移動(dòng)到被SearchViewState.Loading 代表的“加載狀態(tài)”债朵。因此,我們建立了一個(gè)從我們的業(yè)務(wù)邏輯到View的觀察流,每次發(fā)射一個(gè)改變后的Model瀑凝,我們的"狀態(tài)"也會(huì)隨著改變序芦。我們肯定不希望我們的觀察流因?yàn)榫W(wǎng)絡(luò)錯(cuò)誤而終止。因此粤咪,這類錯(cuò)誤被處理為一種被Model代表的狀態(tài)(除去那些致命錯(cuò)誤)谚中。通常情況下,在MVI中可觀察對(duì)象Model不會(huì)被終止(永遠(yuǎn)不會(huì)執(zhí)行onComplete()或onError())寥枝。

對(duì)上面部分做個(gè)總結(jié):SearchInteractor(業(yè)務(wù)邏輯)提供了一個(gè)觀察流Observable<SearchViewState> 宪塔,并且當(dāng)每次狀態(tài)變化的時(shí)候,發(fā)射一個(gè)新的SearchViewState囊拜。

下一步某筐,讓我討論View層長(zhǎng)什么樣子的。View層應(yīng)該做什么?顯然的冠跷,view應(yīng)該去顯示Model南誊。我們已經(jīng)同意身诺,View應(yīng)當(dāng)有一個(gè)像render(model) 這樣的方法。另外抄囚,View需要提供一個(gè)方法給其他層用來(lái)接收用戶輸入的事件霉赡。這些事件在MVI中被稱作 intents 。在這個(gè)例子中幔托,我們僅僅只有一個(gè)intent:用戶可以通過(guò)在輸入?yún)^(qū)輸入字符串來(lái)搜索穴亏。在MVP中一個(gè)好的做法是我們可以為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僅僅提供一個(gè)intent蟆湖,但是,在其他業(yè)務(wù)情況下玻粪,可能需要多個(gè)intent隅津。在第一部分我們討論了為什么單個(gè)render()方法(譯者:渲染方法)是一個(gè)好的方式,如果劲室,你不清楚為什么我們需要單個(gè)render()伦仍,你可以先去閱讀第一部分。在我們具體實(shí)現(xiàn)View層之前很洋,我們先看一下最后搜索頁(yè)面是什么樣的

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) 這個(gè)方法充蓝,我們通過(guò)看,就知道它是干什么的喉磁。在 searchIntent() 方法中我們用到了Jake Wharton’s的RxBindings 庫(kù)谓苟,它使RxJava像綁定可觀察對(duì)象一樣綁定安卓UI控件。 RxSearchView.queryText()創(chuàng)建一個(gè) Observable<String>對(duì)象协怒,每當(dāng)用戶在EditText輸入的一些字符涝焙,發(fā)射需要搜索的字符串。我們用filter()去保證只有當(dāng)用戶輸入的字符數(shù)超過(guò)三個(gè)的時(shí)候孕暇,才開(kāi)始搜索仑撞。并且,我們不希望每當(dāng)用戶輸入一個(gè)新字符的時(shí)候就請(qǐng)求網(wǎng)絡(luò)妖滔,而是當(dāng)用戶輸入完成以后再去請(qǐng)求網(wǎng)絡(luò)(debounce()停留500毫秒隧哮,決定用戶是否輸入完成)。

因此座舍,我們知道對(duì)于這個(gè)頁(yè)面而言沮翔,輸入是searchIntent(),輸出是render()簸州。我們?nèi)绾螐摹拜斎搿钡健拜敵觥奔撸肯旅娴囊曨l將這個(gè)過(guò)程可視化了:

其余的問(wèn)題是誰(shuí)或如何把我們的View的意圖(intent)和業(yè)務(wù)邏輯聯(lián)系起來(lái)?如果你已經(jīng)看過(guò)了上面的視頻歧譬,可以看到在中間有一個(gè)RxJava的操作符 flatMap() 。這暗示了我們需要調(diào)用額外的組件搏存,但是瑰步,我們至今為止還沒(méi)有討論,它就是 Presenter 。Presenter將所有分離的不同點(diǎn)(譯者:這里指Model,View,Intent這三個(gè)點(diǎn))聯(lián)系起來(lái)璧眠。它與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 是什么?這個(gè)是我寫(xiě)的一個(gè)庫(kù)叫 Mosby (Mosby3.0已經(jīng)添加了MVI組件)责静。這篇博客不是為介紹Mosby而寫(xiě)的袁滥,但是,我想對(duì)MviBasePresenter做個(gè)簡(jiǎn)短的介紹灾螃。介紹一下MviBasePresenter如何讓你方便使用的题翻。這個(gè)庫(kù)里面沒(méi)有什么黑魔法。讓我們從lifecycle(生命周期)開(kāi)始說(shuō):MviBasePresenter事實(shí)上沒(méi)有l(wèi)ifecyle(生命周期)腰鬼。有一個(gè) bindIntent() 方法將視圖的意圖(intent)與業(yè)務(wù)邏輯綁定嵌赠。通常,你用flatMap()或switchMap 亦或concatMap()熄赡,將意圖(intent)傳遞給業(yè)務(wù)邏輯姜挺。這個(gè)方法的調(diào)用僅僅在View第一次被附加到Presenter。當(dāng)View重新附加到Presenter時(shí)彼硫,將不會(huì)被調(diào)用(例如炊豪,當(dāng)屏幕方向改變)。

這聽(tīng)起來(lái)很奇怪拧篮,也許有人會(huì)說(shuō):“MviBasePresenter在屏幕方向變化的時(shí)候都能保持词渤?如果是的話,Mosby是如何確贝ǎ可觀察流的數(shù)據(jù)在內(nèi)存中掖肋,而不被丟失?”,這是intent()subscribeViewState() 的就是用來(lái)回答這個(gè)問(wèn)題的赏参。intent() 在內(nèi)部創(chuàng)建一個(gè)PublishSubject ,并將其用作你的業(yè)務(wù)邏輯的“門戶”沿盅。所以實(shí)際上這個(gè)PublishSubject訂閱了View的意圖(intent)可觀察對(duì)象( Observable)把篓。調(diào)用intent(o1)實(shí)際上返回一個(gè)訂閱了o1的PublishSubject。

當(dāng)方向改變的時(shí)候腰涧,Mosby從Presenter分離View韧掩,但是,僅僅只是暫時(shí)的取消訂閱內(nèi)部的PublishSubject窖铡。并且疗锐,當(dāng)View重新連接到Presenter的時(shí)候,將PublishSubject重新訂閱View的意圖(intent)坊谁。

subscribeViewState() 用不同的方式做的是同樣的事情(Presenter到View的通信)。它在內(nèi)部創(chuàng)建一個(gè)BehaviorSubject 作為業(yè)務(wù)邏輯到View的“門戶”滑臊。既然是BahaviorSubject口芍,我們可以從業(yè)務(wù)邏輯收到“模型更新”的信息,即使是目前沒(méi)有view附加(例如雇卷,View正處于返回棧)鬓椭。BehaviorSubjects總是保留最后時(shí)刻的值,每當(dāng)有View附加到上面的時(shí)候关划,它就開(kāi)始重新接收小染,或者將它保留的值傳遞給View。

規(guī)則很簡(jiǎn)單:用intent()去“包裝”所有View的意圖(點(diǎn)擊事件等)贮折。用subscribeViewState()而不是Observable.subscribe(...)裤翩。

MviBasePresenter.png

和bindIntent()對(duì)應(yīng)的是unbindIntents() ,這兩個(gè)方法僅僅會(huì)被調(diào)用一次调榄,當(dāng)unbindIntents()調(diào)用的時(shí)候踊赠,那么View就會(huì)被永久銷毀。舉個(gè)例子振峻,將fragment處于返回棧臼疫,不去永久銷毀view,但是如果一個(gè)Activity結(jié)束了它的生命周期,就會(huì)永久銷毀view扣孟。由于intent()和subscribeViewState()已經(jīng)負(fù)責(zé)訂閱管理烫堤,所以你幾乎不需要實(shí)現(xiàn)unbindIntents()。

那么關(guān)于我們生命周期中的onPause()onResume() 是如何處理的?我認(rèn)為Presenters是不需要關(guān)注生命周期 凤价。如果鸽斟,你非要在Presenter中處理生命周期,比如你將onPause()作為intent利诺。你的View需要提供一個(gè)pauseIntent() 方法富蓄,這個(gè)方法是由生命周期觸發(fā)的,而不是用戶交互觸發(fā)的慢逾,但兩者都是有效的意圖立倍。

總結(jié)

在第二部分,我們討論了關(guān)于Model-View-Intent的基礎(chǔ)侣滩,并且用MVI實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的搜索頁(yè)面口注。讓我們?nèi)腴T。也許這個(gè)例子太簡(jiǎn)單了君珠。你無(wú)法看出MVI的優(yōu)勢(shì)寝志,Model代表狀態(tài)和單向數(shù)據(jù)流同樣適用于傳統(tǒng)的MVP或MVVM。MVP和MVVM都很優(yōu)秀。MVI也許并沒(méi)有它們優(yōu)秀材部。即使如此毫缆,我認(rèn)為MVI幫助我們面對(duì)復(fù)雜問(wèn)題的時(shí)候?qū)憙?yōu)雅的代碼。我們將在這個(gè)系列博客第三部分乐导,討論狀態(tài)減少苦丁。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市兽叮,隨后出現(xiàn)的幾起案子芬骄,更是在濱河造成了極大的恐慌,老刑警劉巖鹦聪,帶你破解...
    沈念sama閱讀 206,723評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件账阻,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡泽本,警方通過(guò)查閱死者的電腦和手機(jī)淘太,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)规丽,“玉大人蒲牧,你說(shuō)我怎么就攤上這事《妮海” “怎么了冰抢?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,998評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)艘狭。 經(jīng)常有香客問(wèn)我挎扰,道長(zhǎng),這世上最難降的妖魔是什么巢音? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,323評(píng)論 1 279
  • 正文 為了忘掉前任遵倦,我火速辦了婚禮,結(jié)果婚禮上官撼,老公的妹妹穿的比我還像新娘梧躺。我一直安慰自己,他們只是感情好傲绣,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布掠哥。 她就那樣靜靜地躺著,像睡著了一般秃诵。 火紅的嫁衣襯著肌膚如雪龙致。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,079評(píng)論 1 285
  • 那天顷链,我揣著相機(jī)與錄音,去河邊找鬼。 笑死嗤练,一個(gè)胖子當(dāng)著我的面吹牛榛了,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播煞抬,決...
    沈念sama閱讀 38,389評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼霜大,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了革答?” 一聲冷哼從身側(cè)響起战坤,我...
    開(kāi)封第一講書(shū)人閱讀 37,019評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎残拐,沒(méi)想到半個(gè)月后途茫,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,519評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡溪食,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評(píng)論 2 325
  • 正文 我和宋清朗相戀三年囊卜,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片错沃。...
    茶點(diǎn)故事閱讀 38,100評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡栅组,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出枢析,到底是詐尸還是另有隱情玉掸,我是刑警寧澤,帶...
    沈念sama閱讀 33,738評(píng)論 4 324
  • 正文 年R本政府宣布醒叁,位于F島的核電站司浪,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏辐益。R本人自食惡果不足惜断傲,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望智政。 院中可真熱鬧认罩,春花似錦、人聲如沸续捂。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,289評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)牙瓢。三九已至劫拗,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間矾克,已是汗流浹背页慷。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,517評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人酒繁。 一個(gè)月前我還...
    沈念sama閱讀 45,547評(píng)論 2 354
  • 正文 我出身青樓滓彰,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親州袒。 傳聞我的和親對(duì)象是個(gè)殘疾皇子揭绑,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評(píng)論 2 345

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