[译]用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帮助咱们面对复杂问题的时候写优雅的代码。咱们将在这个系列博客第三部分,讨论状态减小。

相关文章
相关标签/搜索