Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[译]使用MVI打造响应式APP(二):View层和Intent层 #15

Open
qingmei2 opened this issue Jul 24, 2019 · 0 comments
Open

[译]使用MVI打造响应式APP(二):View层和Intent层 #15

qingmei2 opened this issue Jul 24, 2019 · 0 comments
Labels
MVI-Architecture Model-View-Intent architecture in Android

Comments

@qingmei2
Copy link
Owner

[译]使用MVI打造响应式APP(二):View层和Intent层

原文:REACTIVE APPS WITH MODEL-VIEW-INTENT - PART2 - VIEW AND INTENT
作者:Hannes Dorfmann
译者:却把清梅嗅

上文 中,我们探讨了对Model的定义、与 状态 的关系以及如何在通过良好地定义Model来解决一些Android开发中常见的问题。本文将通过 Model-View-Intent ,即MVI模式,继续我们的 响应式App 构建之旅。

如果您尚未阅读上一小节,则应在继续阅读本文之前阅读该部分。总结一下:以“传统的”MVP为例,请避免写出这样的代码:

class PersonsPresenter extends Presenter<PersonsView> {

  public void load(){
    getView().showLoading(true); // 展示一个 ProgressBar

    backend.loadPersons(new Callback(){
      public void onSuccess(List<Person> persons){
        getView().showPersons(persons); // 展示用户列表
      }

      public void onError(Throwable error){
        getView().showError(error); // 展示错误信息
      }
    });
  }
}

我们应该创建能够反映 状态Model,像这样:

class PersonsModel {
  // 在真实的项目中,需要定义为私有的
  // 并且我们需要通过getter和setter来访问它们
  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层也应该像这样进行定义:

class PersonsPresenter extends Presenter<PersonsView> {

  public void load(){
    getView().render( new PersonsModel(true, null, null) ); // 展示一个 ProgressBar

    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层的render(personsModel)方法,Model就会被成功的渲染在屏幕上。在第一小节中我们同样探讨了 单项数据流 的重要性,同时您的业务逻辑应该驱动该Model。在正式将所有内容环环相扣连接之前,我们先简单了解一下MVI的核心思想。

Model-View-Intent (MVI)

该模式最初被 andrestaltz 在他写的JavaScript框架 cycle.js 中所提出; 从理论(还有数学)上讲,我们这样对Model-View-Intent的定义进行描述:

1.intent()

此函数接受来自用户的输入(即UI事件,比如点击事件)并将其转换为可传递给Model()函数的参数,该参数可能是一个简单的StringModel进行赋值,也可能像是Object这样复杂的数据结构。intent作为意图,标志着 我们试图对Model进行改变

2.model()

model()函数将intent()函数的输出作为输入来操作Model,其函数输出是一个新的Model(状态发生了改变)。

不要对已存在的Model对象进行修改,我们需要的是不可变!对此,在上文中我们已经展示了一个计数器的具体案例,再次重申,不要修改已存在的Model

根据intent所描述的变化,我们创建一个新的Model,请注意,Model()函数是唯一允许对Model进行创建的途径。然后这个新的Model作为该函数的输出——基本上model()函数调用我们App的业务逻辑(可以是交互、用例、Repository......您在App中使用的任何模式/术语)并作为结果提供新的Model对象。

3.view()

该方法获取model()函数返回的Model,并将其作为view()函数的输入,这之后通过某种方式将Model展示出来,view()view.render(model)大体上是一致的。

4.本质

但是我们希望构建的是 响应式的App,不是吗?那么MVI是如何响应式的呢?响应式实际上意味着什么?

这意味着AppUI反映了状态的变更

因为Model反映了状态,因此,本质上我们希望 业务逻辑能够对输入的事件(即intents)进行响应,并创建对应的Model作为输出,这之后再通过调用View层的render(model)方法,对UI进行渲染

5.通过RxJava串联

我们希望我们的数据流的单向性,因此RxJava闪亮登场。我们的App必须通过RxJava保持 数据的单向性响应式 来构建吗?或者必须用MVI模式才能构建吗?当然不,我们也可以写 命令式程序性 的代码。但是,基于事件编程RxJava实在太优秀了,既然UI是基于事件的,因此使用RxJava也是非常有意义的。

本文我们将会构建一个简单的虚拟在线商店App,其UI界面中展示的商品数据,都来源于我们向后台进行的网络请求。

我们可以精确的搜索特定的商品,并将其添加到我们的购物车中,最终App的效果如下所示:

这个项目的源码你可以在Github上找到,我们从实现一个简单的搜索界面开始做起:

首先,就像上文我们描述的那样,我们定义一个Model用于描述View层是如何被展示的—— 这个系列中,我们将用带有 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 类型的对象,它可能是SearchViewState.Error或者其它的一个实例。这只是我个人的偏好,们也可以通过不同的方式定义,例如:

class SearchViewState {
  Throwable error;  // 非空则意味着,出现了一个错误
  boolean loading;  // 值为true意味着正在加载中
  List<Product> result; // 非空意味着商品列表的结果
  boolean SearchNotStartedYet; // true意味着还未开始搜索
}

再次重申,如何定义Model纯属个人喜好,如果你用Kotlin作为编程语言,那么sealed classes是一个不错的选择。

将目光聚集回到业务代码,让我们通过 SearchInteractor 去执行搜索的功能,其输出就是我们之前说过的SearchViewState对象:

public class SearchInteractor {
  final SearchEngine searchEngine; // 执行网络请求

  public Observable<SearchViewState> search(String searchString) {
    // 如果是空的字符串,不进行搜索
    if (searchString.isEmpty()) {
      return Observable.just(new SearchViewState.SearchNotStartedYet());
    }

    // 搜索商品列表
    // 返回 Observable<List<Product>>
    return searchEngine.searchFor(searchString)
        .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()的方法签名:我们将String类型的searchString作为 输入 的参数,以及Observable<SearchViewState>类型的 输出,这意味着我们期望随着时间的推移,可以在可观察的流上会有任意多个SearchViewState的实例被发射。

在我们正式开始查询搜索之前(即SearchEngine执行网络请求),我们通过startWith()操作符发射一个SearchViewState.Loading,这将会使得View在执行搜索时展示ProgressBar

onErrorReturn()会捕获在执行搜索时抛出的所有异常,并且发射出一个SearchViewState.Error——在订阅这个Observable时,我们为什么不去使用onError()回调呢?

这是一个对RxJava认知的普遍误解,实际上,onError()的回调意味着 整个可观察的流进入了不可恢复的状态,因此可观察的流结束了,而在我们的案例中,类似“没有网络连接”的error并非不可恢复的error:这只是我们的Model所代表的另外一个状态。

此外,我们还有另外一个可以转换到的状态,即一旦网络连接可用,我们可以通过 SearchViewState.Loading 跳转到的 加载状态

因此,我们建立了一个可观察的流,这是一个每当状态发生了改变,从业务逻辑层就会发射一个发生了改变的ModelView层的流。

我们不想在网络连接错误时终止这个可观察的流,因此,在error发生时,类似这种可以被处理为 状态error(而不是终止流的那种致命的错误),可以反应为Model,被可观察的流发射。

通常,在MVI中,ModelObservable永远不会被终止(即永远不会执行onComplete()或者onError()回调)。

总结一下,SearchInteractor(即业务逻辑)提供了一个可观察的流Observable<SearchViewState>,每当状态发生了变化,就会发射一个新的SearchViewState

6.View层的职责

接下来我们来讨论一下View应该是什么样的,View层的职责是什么?显然View层应该对Model进行展示,我们已经认可View层应该有类似 render(model) 这样的函数。此外,View应该提供一个给其他层响应用户输入的方法,在MVI中这个方法被称为 intents

在这个案例中,我们只有一个intent:用户可以在输入框中输入一个用于检索商品的字符串进行搜索。MVP中的好习惯是为View层定义一个接口,所以在MVI中我们也可以这样做。

public interface SearchView {

  // 搜索的intent
  Observable<String> searchIntent();

  // 对View层进行渲染
  void render(SearchViewState viewState);
}

我们的案例中View层只提供了一个intent,但通常View拥有更多的intent;在 第一小节 中我们讨论了为什么一个单独的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) // 感谢 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 WhartonRxBinding ,这是一个对Android UI组件提供了RxJava响应式支持的库。

RxSearchView.queryText()创建了一个Observable<String>,每当用户在EditText上输入了一些文字,它就会发射一个对应的字符串;我们通过filter()去保证只有当用户输入的字符数达到三个以上时才进行搜索;同时,我们不希望每当用户输入一个字符,就去请求网络,而是当用户输入结束后再去请求网络(debounce()操作符会停留500毫秒以决定用户是否输入完成)。

现在我们知道了屏幕中的searchIntent()方法就是 输入 ,而render()方法则是 输出。我们如何从 输入 获得 输出 呢,如下所示:

7.连接View和Intent

剩下的问题就是:我们如何将Viewintent和业务逻辑进行连接呢?如果你认真观看了上面的流程图,你应该注意到了中间的 flatMap() 操作符,这暗示了我们还有一个尚未谈及的组件: Presenter ;Presenter负责连接这些点,就和我们在MVP中使用的方式一样。

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

  @Override protected void bindIntents() {
    Observable<SearchViewState> search =
        intent(SearchView::searchIntent)
        // 上文中我们谈到了flatMap,但在这里switchMap更为适用
            .switchMap(searchInteractor::search)
            .observeOn(AndroidSchedulers.mainThread());

    subscribeViewState(search, SearchView::render);
  }
}

什么是 MviBasePresenter, intent()subscribeViewState() 又是什么?这个类是我写的 Mosby 库的一部分(3.0版本后,Mosby已经支持了MVI)。本文并非为了讲述Mosby,但我向简单介绍一下MviBasePresenter是如何的便利——这其中没有什么黑魔法,虽然确实看起来像是那样。

让我们从生命周期开始:MviBasePresenter并未持有任何生命周期,它暴露出一个 bindIntent() 方法以供View层和业务逻辑进行绑定。通常,你通过flatMap()switchMap()或者concatMap()操作符将intent “转移”到业务逻辑中,这个方法仅仅在View层第一次被附加到Presenter中时调用,而当View再次被附加在Presenter中时(比如,屏幕方向发生了改变),将不再被调用。

这听起来有些怪,也许有人会说:

MviBasePresenter在屏幕方向发生了改变后依然能够存活?如果是这样,Mosby如何保证Observable的流不会发生内存的泄漏?

这就是 intent()subscribeViewState() 的作用所在了,intent() 在内部创建一个PublishSubject,就像是业务逻辑的“网关”一样;实际上,PublishSubject订阅了View层传过来的intentObservable,调用intent(o1)实际返回了一个订阅了o1PublishSubject

屏幕发生旋转时,MosbyViewPresenter中分离,但是,内部的PublishSubject只是暂时和View解除了订阅;而当View重新附着在Presenter上时,PublishSubject将会对View层的intent进行重新订阅。

subscribeViewState()方法做的是同样的事情,只不过将顺序调换了过来(PresenterView层的通信)。它在内部创建一个BehaviorSubject作为从业务逻辑到View层的“网关”。

由于它是一个BehaviorSubject,因此,即使此时Presenter没有持有View,我们依然可以从业务逻辑中接收到Model的更新(比如View并未处于栈顶);BehaviorSubjects始终持有它最后的值,并在View重新依附后将其重新发射。

规则很简单:使用intent()来“包装”View层的所有intent,使用subscribeViewState()替代Observable.subscribe().

8.UnbindIntents

bindIntent()相对应的是 unbindIntents() ,该方法只会执行一次,即View被永久销毁时才会被调用。举个例子,将一个Fragment放在栈中,直到Activity被销毁之前,该View一直不会被销毁。

由于intent()subscribeViewState()已经对订阅进行了管理,因此您只需要实现unbindIntents()

9.其它生命周期的事件

那么其它生命周期的事件,比如onPause()onResume()又该如何处理?我依然认为Presenter不需要生命周期的事件,然而,如果你坚持认为你需要将这些生命周期的事件视为另一种形式的intent,您的View可以提供一个pauseIntent(),它是由android生命周期触发,而又不是按钮点击事件这样的由用户交互触发的intent——但两者都是有效的意图。

结语

第二小节中,我们探讨了Model-View-Intent的基础,并通过MVI浅尝辄止实现了一个简单的页面。也许这个例子太简单了,所以你尚未感受到MVI模式的优点:代表 状态Model和与传统MVP或者MVVM相比的 单项数据流

MVPMVVM并没有什么问题,我也并非是在说MVI比其它架构模式更优秀,但是,我认为MVI可以帮助我们 为复杂的问题编写优雅的代码 ,这也正如我们将在本系列博客的 下一小节(第3小节)中探讨的那样——届时我们将针对 状态折叠器 (state reducers)的问题进行探讨,欢迎关注。


系列目录

《使用MVI打造响应式APP》原文

《使用MVI打造响应式APP》译文

《使用MVI打造响应式APP》实战


关于我

Hello,我是却把清梅嗅,如果您觉得文章对您有价值,欢迎 ❤️,也欢迎关注我的博客或者Github

如果您觉得文章还差了那么点东西,也请通过关注督促我写出更好的文章——万一哪天我进步了呢?

@qingmei2 qingmei2 added the MVI-Architecture Model-View-Intent architecture in Android label Jul 24, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
MVI-Architecture Model-View-Intent architecture in Android
Projects
None yet
Development

No branches or pull requests

1 participant