原文:《REACTIVE APPS WITH MODEL-VIEW-INTENT - PART1 - MODEL》
<http://hannesdorfmann.com/android/mosby3-mvi-1>
作者:Hannes Dorfmann <http://hannesdorfmann.com>
译者:却把清梅嗅 <https://github.com/qingmei2>

有朝一日,我突然发现我对于Model层的定义 全部是错误的,更新了认知后,我发现曾经我在Android平台上主题讨论中的那些困惑或者头痛都消失了。

从结果上来说,最终我选择使用 RxJava 和 Model-View-Intent(MVI) 构建 响应式的APP
,这是我从未有过的尝试——尽管在这之前我开发的APP也是响应式的,但响应式编程
的体现与这次实践相比,完全无法相提并论,在接下来我将要讲述的一系列文章中,你也会感受到这些。但作为系列文章的开始,我想先阐述一个观点:

所谓的Model层到底是什么,我之前对Model层的定义出现了什么问题?

我为什么说 我对Model层有着错误的理解和使用方式 呢?当然,现在有很多架构模式将View层和Model层进行了分离,至少在Android
开发的领域,最著名的当属Model-View-Controller (MVC)、Model-View-Presenter (MVP)和
Model-View-ViewModel (MVVM)——你注意到了吗?这些架构模式中,Model都是不可或缺的一环,但我意识到 在绝大数情况下,我根本没有
Model。

举例来说,一个简单的从后端拉取Person列表情况下,传统的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
到底是指什么呢?是指后台的网络请求吗?不,那只是业务逻辑。是指请求结果的用户列表吗?不,它和ProgressBar、错误信息的展示一样,仅仅只代表了View
层所能展示内容的一小部分而已。

那么,Model层究竟是指什么呢?

从我个人理解来说,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层持有了一个Model,并且能够借助它对屏幕上的控件进行rendered(渲染)。这并非什么新鲜的概念,Trygve Reenskaug
在1979年时,其对最初版本的MVC定义中具有相似的概念:View观察Model的变化。

然而,MVC这个术语被用来描述太多种不同的模式,这些模式与Reenskaug在1979年制定的模式并不完全相同。比如后端开发人员使用MVC框架,iOS有
ViewController,到了Android领域MVC又被如何定义了呢?Activity是Controller吗? 那这样的话ClickListener
又算什么呢?如今,MVC这个术语变成了一个很大的误区,它错误地理解和使用了Reenskaug最初制定的内容——这个话题到此为止,再继续下去整个文章就会失控了。

言归正传,Model的持有将会解决许多我们在Android开发中经常遇到的问题:

* 1.状态问题
* 2.屏幕方向的改变
* 3.在页面堆栈中导航
* 4.进程终止
* 5.单向数据流的不变性
* 6.可调试和可重现的状态
* 7.可测试性
要讨论这些关键的问题,我们先来看看“传统”的MVP和MVVM的实现代码中如何处理它们,然后再谈Model如何跳过这些常见的陷阱。

<>1.状态问题

响应式App,这是最近非常流行的话题,不是吗?所谓的 响应式App 就是 应用会根据状态的改变作出UI的响应,这句话里有一个非常好的单词:状态
。什么是状态呢?大多数时间里,我们将状态 描述为我们在屏幕中看到的东西,例如当界面展示ProgressBar时的loading state。

很关键的一点是,我们前端开发人员倾向专注于UI。这不一定是坏事,因为一个好的UI体验决定了用户是否会用你的产品,从而决定了产品能否获得成功。但是看看上述的
MVP示例代码(不是使用了PersonModel的那个例子),这里UI的状态由Presenter进行协调,Presenter负责告诉View层如何进行展示。

MVVM亦然,我想在本文中对MVVM的两种实现方式进行区分:第一种依赖DataBinding库,第二种则依赖RxJava;对于依赖DataBinding
的前者,其状态被直接定义于ViewModel中:
class PersonsViewModel { ObservableBoolean loading; // 省略... public void
load(){ loading.set(true); backend.loadPersons(new Callback(){ public void
onSuccess(List<Person> persons){ loading.set(false); // 省略其它代码,比如对persons进行渲染 }
public void onError(Throwable error){ loading.set(false); // 省略其它代码,比如展示错误信息 }
}); } }
使用RxJava实现MVVM的方式中,其并不依赖DataBinding引擎,而是将Observable和UI的控件进行绑定,例如:
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) //
实现方式并不惟一 } // 在View层订阅它 (比如 Activity / Fragment) public Observable<Boolean>
loading(){ return loading; } // 在View层订阅它 (比如 Activity / Fragment) public
Observable<List<Person>> persons(){ return persons; } // 每当触发此操作 (即调用 onNext())
,加载Persons数据 public PublishSubject loadPersonsCommand(){ return
loadPersonsCommand; } }
当然,这些代码并非完美,您的实现方式可能截然不同;我想说明的是,通常在MVP或者MVVM中,状态 是由ViewModel或者Presenter进行驱动的。

这导致下述情况的发生:

*
1.业务逻辑本身也拥有了状态,Presenter(或者ViewModel)本身也拥有了状态(并且,你还需要通过代码去同步它们的状态使其保持一致),同时,
View可能也有自己的状态(比方说,调用View的setVisibility()方法设置其可见性,或者Android系统在重新创建时从bundle恢复状态)。

*
2.Presenter(或ViewModel)有任意多个输入(View层触发行为并交给Presenter处理),这是ok的,但同时Presenter
也有很多输出(或MVP中的输出通道,如view.showLoading()或view.showError();在MVVM中,ViewModel
的实现中也提供了多个Observable,这最终导致了View层,Presenter层和业务逻辑中状态的冲突,在处理多线程的时候,这种情况更明显。

在好的情况下,这只会导致视觉上的错误,例如同时显示加载指示符(“加载状态”)和错误指示符(“错误状态”),如下所示:



在最糟糕的情况下,您从崩溃报告工具(如Crashlytics)接收到了一个严重的错误报告,但您无法重现这个错误,因此也几乎无从着手去修复它。

如果从 底层 (业务逻辑层)到 顶层 (UI视图层),有且仅有一个真实描述状态的源,会怎么样呢?事实上,我们已经在文章的开头谈论Model
的时候,就已经通过案例,把相似的概念展示了出来:
class PersonsModel { 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; } }
你猜怎么了? Model映射了状态,当我想通了这点,许多状态相关的问题迎刃而解(甚至在编码之前就已经被避免了);现在Presenter层变得只有一个输出了:

getView().render(PersonsModel)

它对应了一个数学上简单的函数,比如f(x) = y,对于多个输入的函数,对应的则是f(a,b,c),但也是一个输出。

并非对所有人来说数学都是香茗,就好像数学家并不清楚bug是什么——但软件工程师需要去品尝它。

了解Model到底是什么以及如何建立对应的Model非常重要,因为最终Model可以解决 状态问题。

<>2.屏幕方向的改变

译者注:针对 屏幕旋转后的状态回溯 这个问题,已经可以通过Google官方发布的ViewModel组件进行处理,开发者不再需要为此烦恼,但本章节仍值得一读。

Android设备上的 屏幕旋转 是一个有足够挑战性的问题;忽视它是一个最简单的解决方案,即 每次屏幕旋转,都对数据重新进行加载
。这确实行之有效,大多数情况下,您的APP也在离线状态下工作,其数据来源于数据库或者其它本地缓存,这意味着屏幕旋转后的数据加载速度是很快的。

但是,个人而言我不喜欢看到加载框,哪怕加载速度是毫秒级别的,因为我认为这并非完美的用户体验,因此大家(包括我)开始使用MVP,这其中包括了
保留性的Presenter——这样就可以 在屏幕旋转时分离和销毁View层,而Presenter则会保存在内存中不会被销毁,然后View层会再次连接到
Presenter。

使用RxJava的MVVM也可以实现相同的概念,但请牢记,一旦View对ViewModel取消了订阅,可观察的流就会被销毁,这个问题你可以用Subject
解决;对于DataBinding构建的MVVM来讲,ViewModel由DataBinding直接绑定到View
层,为了避免内存泄露,需要我们在屏幕旋转时及时销毁ViewModel。

对于 保留性的Presenter 或者 ViewModel 的问题是: 我们如何将View的状态在屏幕旋转之后回溯,保证View和Presenter
再次回到之前相同的状态?我编写了一个名为Mosby <https://github.com/sockeqwe/mosby> 的MVP库,其包含一个名为
ViewState的功能,它基本上将业务逻辑的状态与View同步。 Moxy <https://github.com/Arello-Mobile/Moxy>
,另一个MVP库,提出了一个非常有趣的解决方案——通过使用commands在屏幕方向更改后重现View的状态:



针对View层状态的问题,我很确定还有其他的解决方案。让我们退后一步,归纳一下这些库试图解决的问题:那就是我们已经讨论过的 状态问题。

再次重申,我们通过一个 能反映当前状态的Model 和一个渲染Model的方法 解决了这个问题,就像调用
getView().render(PersonsModel)一样简单。

<>3.在页面堆栈中导航

当View不再使用时,是否还有保留Presenter(或ViewModel)的必要?比如,用户跳转到了另外一个界面,这导致Fragment(View)被另外的
Fragment给replace了,因此Presenter已经不在被任何View持有。

如果没有View层和Presenter进行关联,Presenter自然也无法根据业务逻辑,将最新的数据反映在View
上。但如果用户又回来了怎么办(比如按下后退按钮),是重新加载数据 还是 重用现有的Presenter?——这看起来像是一个哲学问题。

通常用户一旦回到之前的界面,他会期望回到之前的界面继续操作。这仍然像是第二小节关于View层 状态恢复 的问题,解决方案简明扼要:当用户返回时,我们得到
代表状态的Model ,然后只需调用 getView().render(PersonsModel) 对View层进行渲染。

<>4.进程终止

进程终止是一件坏事,并且我们需要依赖一些库以帮助我们在进程终止后对状态进行恢复——我认为这是Android开发中常见的一种误解。

首先,进程终止的原因只有一个,并且有足够充分的理由——Android操作系统需要更多资源用于其他应用程序或节省电池。
如果你的APP处于前台并且正在被用户主动使用时,这种情况永远不会发生
,因此,遵纪守法,不要与平台作斗争了(就是不要执拗于所谓的进程保活了)。如果你真的需要在后台进行一些长时间的工作,请使用Service
,这也是向操作系统发出信号,告知您的App仍处于“主动使用状态”的唯一方式 。

如果进程终止了,Android会提供一些回调以供 保存状态,比如onSaveInstanceState()——没错,又是 状态 。我们应该将View
的信息保存在Bundle中吗?我们是否也应该把Presenter中的状态保存到Bundle
中?那么业务逻辑的状态呢?又是老生常谈的问题,就和上面三个小节谈到的一样。

我们只需要一个代表整个状态的Model类,我们很容易将Model保存在Bundle中并在之后对它进行恢复。但是,我个人认为大部分情况下最好不保存状态,而是
重新加载整个界面,就像我们第一次启动App一样。 想想显示新闻列表的 NewsReader App。
当App被杀掉,我们保存了状态,6小时后用户重新打开App并恢复了状态,我们的App可能会显示过时的内容。因此,这种情况下,也许不存储Model
和状态、而对数据重新加载才是更好的策略。

<>5.单向数据流的不变性

在这里我不打算讨论不变性(immutabiliy)的优势,因为有很多资源讨论这个问题。我们想要一个不可变的Model
(代表状态)。为什么?因为我们想要唯一的状态源,在传递Model时,我们不希望App中的其他组件可以改变我们的Model或者State。

让我们假设编写一个简单的计数器App,它具有递增和递减的功能按钮,并在TextView中显示当前计数器值。 如果我们的Model
(在这种情况下只是计数器值,即一个整数)是不可变的,那么我们如何更改计数器?

我很高兴被问到这个问题,按钮被点击时,我们并非直接操作TextView。我的建议是:

* 1.我们的View层应该有一个类似view.render(...)的方法;
* 2.我们的Model是不可变的,因此不可直接修改Model;
* 3.View的渲染有且只有一个来源:即业务逻辑。
我们将点击事件 下沉 到业务逻辑层。业务逻辑知道当前的Model(例如,持有一个私有的成员Model,它代表着当前的状态),
这之后根据旧的Model,创建一个新的带有增量/减量值的Model。



这样我们建立了一个 单向数据流,业务逻辑作为单一源用于创建不可变的Model
实例,但对于一个计数器来讲未免有点小题大做,不是吗?诚然,是的,计数器只是一个简单的应用程序。大多数应用程序都是以简单的应用程序开始,但复杂性增长很快——从我的角度来看,
单向数据流和不可变模型是必要的,这会使简单的应用程序,在复杂性递增的同时,依然保持着简单(对开发者而言)。

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

此外,单向数据流保证了我们的应用程序易于调试
。下次我们从Crashlytics获得崩溃报告时,我们可以轻松地重现并修复此崩溃,因为所有必需的信息都已附加到崩溃报告中了。

什么叫做必需的信息?那就是当前的Model
和用户用户在崩溃发生时想要执行的操作(比如,点击减量按钮)。这就是我们重现这次崩溃所需的全部信息,这些信息非常容易收集并附加在崩溃报告中。

如果没有单项数据流(比如,对EventBus的滥用,或者将CounterModels的私有域暴露出来),或者没有不变性(这会导致我们不知道谁实际更改了
Model),那么bug的复现就没那么容易了。

<>7.可测试性

“传统”的MVP或MVVM提高了应用程序的可测试性。MVC也是可测试的:没有人说我们必须将所有业务逻辑放入Activity中。使用表示状态的Model
,我们可以简化单元测试的代码,因为我们可以简单地检查assertEqual(expectedModel,model)。这使我们避免了许多必须要Mock的对象。

此外,这也减少了很多验证的测试,即某些方法是否被调用(比如Mockito.verify(view, times(1)).showFoo()
),最终,这使得我们的单元测试代码更具可读性,易于理解并且易于维护,因为我们不必处理很多实际代码的实现细节。

<>总结

在这个博客文章系列的第一部分中,我们谈了很多关于理论的东西。我们真的需要关于专门讨论Model的博客吗?

我认为初步地理解Model的确很重要,这也有助于我们避免一些会遇到的问题。Model并不意味着业务逻辑,它是生成Model
的业务逻辑(比如,一次交互,一个用例,一个仓库或者你在APP中调用的任何东西)。

在接下来的第二部分中,当我们最终使用Model-View-Intent构建一个响应式App 时,我们将看到Model
的实际应用。演示的APP是一个虚构的在线商店的应用程序,敬请关注。



<>系列目录

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

* Part 1: Model
<http://hannesdorfmann.com/android/mosby3-mvi-1>
* Part 2: View and Intent <http://hannesdorfmann.com/android/mosby3-mvi-2>
* Part 3: State Reducer <http://hannesdorfmann.com/android/mosby3-mvi-3>
* Part 4: Independent UI Components
<http://hannesdorfmann.com/android/mosby3-mvi-4>
* Part 5: Debugging with ease
<http://hannesdorfmann.com/android/mosby3-mvi-5>
* Part 6: Restoring State
<http://hannesdorfmann.com/android/mosby3-mvi-6>
* Part 7: Timing (SingleLiveEvent problem)
<http://hannesdorfmann.com/android/mosby3-mvi-7>
* Part 8: In-App Navigation
<http://hannesdorfmann.com/android/mosby3-mvi-8>
《使用MVI打造响应式APP》译文

* [译]使用MVI打造响应式APP(一):Model到底是什么
<https://github.com/qingmei2/android-programming-profile/blob/master/src/Android-MVI/%5B%E8%AF%91%5D%E4%BD%BF%E7%94%A8MVI%E6%89%93%E9%80%A0%E5%93%8D%E5%BA%94%E5%BC%8FAPP%5B%E4%B8%80%5D%3AModel%E5%B1%82%E5%88%B0%E5%BA%95%E4%BB%A3%E8%A1%A8%E4%BB%80%E4%B9%88.md>
* [译]使用MVI打造响应式APP[二]:View层和Intent层
<https://github.com/qingmei2/android-programming-profile/blob/master/src/Android-MVI/%5B%E8%AF%91%5D%E4%BD%BF%E7%94%A8MVI%E6%89%93%E9%80%A0%E5%93%8D%E5%BA%94%E5%BC%8FAPP%5B%E4%BA%8C%5D%3AView%E5%B1%82%E5%92%8CIntent%E5%B1%82.md>
* [译]使用MVI打造响应式APP[三]:状态折叠器
<https://github.com/qingmei2/android-programming-profile/blob/master/src/Android-MVI/%5B%E8%AF%91%5D%E4%BD%BF%E7%94%A8MVI%E6%89%93%E9%80%A0%E5%93%8D%E5%BA%94%E5%BC%8FAPP%5B%E4%B8%89%5D%3AStateReducer.md>
* [译]使用MVI打造响应式APP[四]:独立性UI组件
<https://github.com/qingmei2/android-programming-profile/blob/master/src/Android-MVI/%5B%E8%AF%91%5D%E4%BD%BF%E7%94%A8MVI%E6%89%93%E9%80%A0%E5%93%8D%E5%BA%94%E5%BC%8FAPP%5B%E5%9B%9B%5D%3AIndependentUIComponents.md>
* [译]使用MVI打造响应式APP[五]:轻而易举地Debug
<https://github.com/qingmei2/android-programming-profile/blob/master/src/Android-MVI/%5B%E8%AF%91%5D%E4%BD%BF%E7%94%A8MVI%E6%89%93%E9%80%A0%E5%93%8D%E5%BA%94%E5%BC%8FAPP%5B%E4%BA%94%5D%3ADebuggingWithEase.md>
* [译]使用MVI打造响应式APP[六]:恢复状态
<https://github.com/qingmei2/android-programming-profile/blob/master/src/Android-MVI/%5B%E8%AF%91%5D%E4%BD%BF%E7%94%A8MVI%E6%89%93%E9%80%A0%E5%93%8D%E5%BA%94%E5%BC%8FAPP%5B%E5%85%AD%5D%3ARestoringState.md>
* [译]使用MVI打造响应式APP[七]:掌握时机(SingleLiveEvent问题)
<https://github.com/qingmei2/android-programming-profile/blob/master/src/Android-MVI/%5B%E8%AF%91%5D%E4%BD%BF%E7%94%A8MVI%E6%89%93%E9%80%A0%E5%93%8D%E5%BA%94%E5%BC%8FAPP%5B%E4%B8%83%5D%3ATiming%2CSingleLiveEventProblem.md>
* [译]使用MVI打造响应式APP[八]:导航
<https://github.com/qingmei2/android-programming-profile/blob/master/src/Android-MVI/%5B%E8%AF%91%5D%E4%BD%BF%E7%94%A8MVI%E6%89%93%E9%80%A0%E5%93%8D%E5%BA%94%E5%BC%8FAPP%5B%E5%85%AB%5D%3ANavigation.md>
《使用MVI打造响应式APP》实战

* 实战:使用MVI打造响应式&函数式的Github客户端 <https://github.com/qingmei2/MVI-Rhine>
<>关于我

Hello,我是却把清梅嗅 <https://github.com/qingmei2>,如果您觉得文章对您有价值,欢迎 ❤️,也欢迎关注我的博客
<https://www.jianshu.com/u/df76f81fe3ff>或者Github <https://github.com/qingmei2>。

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

* 我的Android学习体系 <https://github.com/qingmei2/android-programming-profile>
* 关于文章纠错
<https://github.com/qingmei2/Programming-life/blob/master/error_collection.md>
* 关于知识付费
<https://github.com/qingmei2/Programming-life/blob/master/appreciation.md>

友情链接
ioDraw流程图
API参考文档
OK工具箱
云服务器优惠
阿里云优惠券
腾讯云优惠券
华为云优惠券
站点信息
问题反馈
邮箱:ixiaoyang8@qq.com
QQ群:637538335
关注微信