[译]为何使用MVI模式(MVI编写响应式安卓APP入门系列第一部分MODEL)

我曾经有一个瞬间觉的个人Model定义全都是错的。通过在各类安卓开发论坛也好主题也罢的讨论和头疼的研究。不管如何,最终我选择使用rxjava和Model-View-Intent(MVI)的方式构建响应式的安卓应用程序,就像这种组合我之前是没有尝试过同样,我建立是十分被动的。固然,你也会,可是,你会比我好不少,由于,我将写一系列文章来介绍这个模式和用法。在第一节,也就是这篇文章,咱们来讲说咱们的Model出现了什么问题?前端

我为何说我之前定义的Model全都是错的咧?诚然,有不少模式将"View"和"Model"分离。在安卓开发领域,最出名的当属Model-View-Controller(MVC),Model-View_Presenter(MVP)和Model-View-ViewModel(MVVM)。你能够从名字看出什么东西么?他们都有Model。可是,我发现大多数时间,我根本没有用Model。java

例子:仅仅是在后台加载一个persons的列表,一个传统的MVP模式的代码是这样的:数据库

class PersonsPresenter extends Presenter<PersonsView> {

  public void load(){
    getView().showLoading(true); // 显示一个加载进度条

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

      public void onError(Throwable error){
        getView().showError(error); // 显示错误信息
      }
    });
  }
}
 
复制代码

可是到底什么是"Model"?后台请求是Model?不是,Model应当是业务逻辑。它是做为结果的列表?不是,它仅仅只作一件事情,就是咱们View显示所须要的东西,像加载指示器或错误信息。所以,真正的Model“长”什么样的?后端

若是按照我对View的理解,那么,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) ); // 显示一个加载进度条

    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。这个概念其实不是什么新概念。最开始的被Trygve Reenskaug在1979年定义的MVC模式的Model的定义几乎一致:View观察Model的变化。不幸的是,MVC这个术语被滥用来描述太多不一样的模式,它们与最原始的MVC定义有了出入。例如,后端工程师使用MVC框架,iOS工程师有ViewController,在安卓开发中MVC的真正含义是什么?Activities是Controller?那么ClickListener意味着什么?如今MVC与最初被Reenskaug定义的MVC来说,这个术语被误解,滥用和错误使用。关于MVC的讨论就此打住,在讨论下去文章就要失控翻车了。app

让咱们回到我刚开始说的地方。Model须要解决咱们在安卓开发中常常遇到的问题:框架

  1. 状态问题
  2. 屏幕方向问题
  3. 在页面堆栈中导航
  4. 进程死亡
  5. 单向数据流的不变性
  6. 可调试和可重现的状态
  7. 测试

让咱们讨论的上面这些点,并研究传统的MVP和MVVM如何处理这些内容,最后,在探究到底什么样的Model能够帮助避免共性的陷阱。dom

1.状态问题

响应式App,能够说最近很是流行。难道不是么?所谓的响应式App应该就是会根据应用的状态改变,来改变UI。这里还有一个单词:"State(上文译为状态)"。什么是"State(上文译为状态)"?大多数时间咱们描述“State(上文译为状态)”,就是咱们从屏幕上看到的东西,好比说在屏幕上显示一个ProgressBar 就是“加载状态”。最关键的地方:咱们的前端开发者趋向于关注UI。这明显不是一件坏事,由于一个好的UI决定了用户会不会用大家家的产品,从而决定了产品能不能成功。可是,咱们看一下上面最基本的MVP示例代码(不是用PersonsModel的例子,是最上面的例子)。Ui的状态被Presenter协调,Presenter决定了View应该显示什么内容。MVVM也是一样的。在这篇博客中我简单区分两种MVVM实现:第一种是用到了Android的data binding,第二种是用到RxJava。在用data binding实现的MVVM这种方式下,状态直接被定义到了ViewModel里面:函数

class PersonsViewModel {
  ObservableBoolean loading;
  // ... Other fields left out for better readability

  public void load(){

    loading.set(true);

    backend.loadPersons(new Callback(){
      public void onSuccess(List<Person> persons){
      loading.set(false);
      // ... other stuff like set list of persons
      }

      public void onError(Throwable error){
        loading.set(false);
        // ... other stuff like set error message
      }
    });
  }
}
复制代码

在使用RxJava实现的MVVM中,咱们不须要使用data binding引擎,而是将Observable绑定到View中的UI Widget,例如:

class RxPersonsViewModel {
  private PublishSubject<Boolean> loading;
  private PublishSubject<List<Person> persons;
  private PublishSubject loadPersonsCommand;

  public RxPersonsViewModel(){
    loadPersonsCommand.flatMap(ignored -> backend.loadPersons())
      .doOnSubscribe(ignored -> loading.onNext(true))
      .doOnTerminate(ignored -> loading.onNext(false))
      .subscribe(persons)
      // Could also be implemented entirely different
  }

  // Subscribed to in View (i.e. Activity / Fragment)
  public Observable<Boolean> loading(){
    return loading;
  }

  // Subscribed to in View (i.e. Activity / Fragment)
  public Observable<List<Person>> persons(){
    return persons;
  }

  // Whenever this action is triggered (calling onNext() ) we load persons
  public PublishSubject loadPersonsCommand(){
    return loadPersonsCommand;
  }
}
复制代码

固然,这只是一个代码片断不是一个完整的代码,你实现的可能看起来彻底不同。重点是一般在MVP和MVVM中,状态由Presenter或ViewModel驱动。

这致使了下面几个问题:

  1. 业务逻辑有了本身的状态,Presenter(或ViewModel)有了本身的状态(你须要同步你的业务逻辑状态,和你的Presenter的状态,二者须要保持一致)而且View可能也有本身的状态(举个栗子,您直接在视图中设置可见性,或者Android自己在从bundle中恢复状态)
  2. Presenter(或者ViewModel)有任意多的输入(View的触发,被Presenter处理),这是能够理解的,可是Presenter也有不少的输出(或输出一些像 view.showLoading()view.showError() 在MVP或ViewModel都提供观察)那么这种状况会致使View,Presenter和业务逻辑的状态冲突,这种现象在多线程下尤其突出。

在最好的状况下,这只会致使可见的错误,例如像这样同时显示加载指示符(“加载状态”)和错误指示符(“错误状态”)

plaid app.gif

在最坏的状况下,你有一个像Crashlytics(理解成bugly)这样的崩溃报告工具报告给你的严重的错误,你没法重现,所以几乎不可能修复。

若是,咱们从底层(业务逻辑)到顶层(VIew)有且仅有一个状态源。其实,咱们最开始展现的第二个例子就是一个很接近这个概念的例子。

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也就只有一个明确的输出:getView().render(PersonsModel) .这反应了一个简单的数学函数像f(x)=y (也能够有多个输入,例如f(a,b,c),但只有一个输出)。数学并非全部的人都擅长,可是,数学家不知道什么是Bug。软件工程师咧。

理解什么是"Model",而且知道model如何正确的定义,是十分重要的,由于到最后Model将解决"状态问题"。

2.屏幕方向改变

安卓屏幕方向改变是一个有挑战性性的问题。最简单的方法是直接忽略这个问题。当屏幕方向改变的时候,从新加载全部的东西。这是彻底有效的解决方法。大多数时间,你的App在离线状态下工做,数据是存储在你的本地数据库或者其余的本地缓存。所以,当屏幕的方向发生改变,加载数据是很快的、然而,我我的不喜欢看到loading指示器(大神都是有点各类小脾气的),尽管它可能只出现几微秒的时间(这里应该用了夸张的修辞手法),由于在我看来这不是一个无缝的用户体验。所以,不少人(包括我)开始使用带有“固定的presenter”的MVP。所以View能够在屏幕方向旋转的时候被分离(被销毁),而presenter将被保留在内存中,随后,咱们的View和Presenter将会被从新链接。在用RxJava实现的MVVM中有相同的概念,可是,咱们须要记在心中的是一旦View被它的ViewModel退订那么观察流就被破坏。例如,你能够用Subjects来解决这个问题。在使用data binding实现的MVVM中ViewModel是经过data binding 引擎直接绑定在View上的。去避免当咱们改变屏幕方向而致使的内存泄露。

可是固定的Presenter(或者ViewModel)有一个问题是:当屏幕旋转的时候,咱们如何将View的状态退回旋转前的状态,也就是说,咱们的View和Presenter是否处在相同的状态?我写了一个MVP库叫作Mosby 带有一个功能叫作ViewState ,用来同步业务逻辑和View的状态。Moxy ,另外一个MVP库,用了一种有趣的方式解决了这个问题,解决的方法就是用到了"命令(原文为commands)"去在屏幕旋转之后,重建View的状态:

moxy.gif

我能够十分肯定的是,确定有其余方法来解决这个问题。让咱们退一步来讲,咱们总结一下上面说到的库的解决方法:他们试图解决咱们一直在讨论的状态问题。

因此,再次强调,当有一个可以反应确切的"状态"的"Model",确定只有一个方法去"渲染(原文为render)"这个"Model"解决这个问题,而且是经过一种简单的如调用 getView().render(PersonsModel) 同样。

3.在页面堆栈中导航

Presenter(或者ViewModel)须要去维护何时View不使用么?举个栗子,若是,Fragment(View)将被另外一个Fragment替换掉,由于用户导航到另外的页面,那么这个将没有View附属到Presenter里。若是没有View没有Presenter显然不可能用最新的从业务逻辑里出来的数据去更新View。若是用户返回(例如,用户按了返回按钮)?去从新加载数据或复用已经存在的Presenter?这是一道哲学题。一般的一旦用户返回先前的页面,他指望回到他原来阅读的地方。这是个最基本的“重置View状态的问题”,咱们刚刚在2中也讨论了这个问题。因此富有策略的解决方案:当"Model"表明一种状态,咱们仅仅须要当用户返回时,调用getView().render(PersonModel) 去渲染视图就能够了。

4.进程死亡

我认为这是个安卓开发广泛错误的理解,就是进程死亡是意见坏的事情,而且在进程死亡之后,咱们须要库去帮助咱们重启状态(例如Presenters或者ViewModels)。第一,一个进程死亡发生的缘由是:安卓操做系统须要更多的资源去给其余的App或者为了省电。可是,若是你的应用程序处于前台,正在被你的用户使用是决定不可能出现进程死亡的。所以,作个好市民,不要在和平台对战了(这里的意思是不要再瞎折腾进程包活了)。若是你真的须要在后台长时间运行的一些工做,请用 Service ,在安卓操做系统中,这是惟一的一种方式向系统发出你的应用程序仍然被使用的信号。若是一个进程死亡发生,安卓提供了一些回调像onSaveInstanceState() 去保存状态。State又出现了。咱们应该保存咱们的View信息到Bundle里么?咱们的Presenter的状态是否是也要存储到Bundle里?那么业务逻辑的状态要不要存?咱们先前也一直讨论这个问题:刚才1.2.3点都在讨论这个问题。咱们仅仅须要一个Model类,这个Model类表明了整个状态。那么存储到Bundle里,就变得很简单了。然而,我我的意见认为大多数时间咱们不存储状态数据,而选择像咱们启动App时候从新加载整个屏幕,彷佛更好。考虑一下新闻阅读软件显示新闻列表,当咱们App六小时之前被杀死,咱们存储了的新闻状态,当用户从新打开咱们的App的时候,咱们六小时前存储的状态被从新显示出来,很显然新闻已通过期了。也许在这种场景下,不去存储状态(Model/State),而去从新加载数据是更好的选择。

5. 单向不可变的数据流

我这里不去讨论不变性(immutabiliy)的先进性,由于有不少资源讨论这个问题。咱们须要一个不变的“Model”(表明状态)。为啥?由于咱们想要惟一的来源。当咱们传递Model对象的时候,咱们不想要在咱们应用中其余组件去改变咱们的Model/状态(State)。让我想象一下咱们正在写一个简单的“计数器”的安卓应用程序,这个有一个增量和一个减量按钮,而且在一个TextView中显示当前技术的值。若是咱们的Model(就是计数的值,一个Integer)是不可变的,咱们如何去更改计数器?我要告诉你,咱们不直接经过按钮点击来控制TextView。一些建议:第一,咱们的View应该有一个view.render(...).第二,咱们的Model是不可变的,所以不可能直接修改Model。第三,有且只有一个来源:业务逻辑。咱们让点击事件“下沉”到业务逻辑层。业务逻辑知道了当前的Model(例如,当前Model有一个私有域)而且将根据旧的Model,建立一个新的带有增量/减量值的Model。

counter

经过这样作,咱们确信有单向的数据流,并将业务逻辑做为建立不可变模型实例的单一的来源。对于一个计数器来说有点过小题大作。难道不是么?是的,一个计数器是一个很是简单的程序。大多数的App都是从一个简单的程序变的复杂起来。我认为,一个单向的数据流和一个不变的Model是十分必要的,当咱们工程变复杂的时候,开发将依然是简单的。

6. 可调试和可重现的状态

此外,单向数据流保证了咱们的APP的调试很是简单。下次若是有新的crash报告从Crashlytics(感受相似与Bugly)传过来,咱们能够很快速的修复Crash。由于全部须要的信息都会在crash报告里面。什么是“须要的信息”?就是咱们须要的当前Model和用户执行了什么样的操做而致使的八阿哥(例子:点击减量按钮)。这就是咱们须要的信息,并且这些信息很显然,是十分容易附加到Crash报告中的。若是,数据流不是单向的,那么实现起来就有点困难。(例如:一些人乱用EventBus,而且将CounterModels暴露出来。译者:EventBus没用过,因此,这里可能看起来怪怪的原话是someone misuses an EventBus and fires CounterModels out into the wild )或不具备不变性(这样会致使咱们不能肯定谁改变了Model)。

7. 可测试

"传统"MVP或者MVVM改善了应用程序的可测试性。MVC也是可测试的:没人告诉咱们业务逻辑必定要放在activity里。当Model表明状态,咱们能够简化咱们的集成测试代码,例如,咱们能够简单的检查assertEquals(expectedModel, model) 。这让咱们除了Model之外的全部对象都不用mock。另外,这能够消除了方法的许多验证测试,例如 Mockito.verify(view, times(1)).showFoo() 。最后,它可让咱们的测试代码可读性更好,更容易理解,更好的可维护性,咱们不须要纠结于如何实如今代码中实现一些细节。

##总结

做为这个系列的第一篇博客,咱们讨论了不少关于理论的东西。咱们真的须要花4千多字介绍Model么?我认为理解Model的实现是十分重要的基础,有助于防止一些问题,不然容易翻车。Model不意味着业务逻辑,它是生成Model的业务逻辑(例如,一个交互,一个用例,一个仓库或者你在APP中调用的任何东西(原文:Model doesn’t mean business logic. It’s the business logic (i.e. an Interactor, a Usecase, a Repositor or whatever you call it in your app) that produces a Model.)。在第二部分,咱们将要将咱们学到的Model理论,用到Model-View-Intent上,来构建响应式应用程序。下面展现的,简单的在线商城软件将是咱们之后去实现的一个例子。你能够指望在第二部分了。 敬请关注。

shop dome
相关文章
相关标签/搜索