我意识到,一直以来我构建 Model 类的方式是有问题的,当我解决它们之后,许多与 Android 平台相关的痛点与槽点也都消失不见。除此之外,我也终于能够使用 RxJava 与 Model-View-Intent(MVI) 模型构建 响应式应用 (Reactive Apps)了。
尽管在这之前也可以构建 响应式 应用,但与此相比并不在一个层级。在这一系列的博文中,我将会介绍与此相关的内容。第一部分,我们来讨论一下 Model ,为什么说, Model 很重要。
为什么说,我构建 Model 类的方式是有问题的呢?有许多的设计模式用于将 View 层与 Model 层分离,在 Android 开发中最常见的就是 Model-View-Controller(MVC) 、 Model-View-Presenter(MVP) 与 Model-View-ViewModel(MVVM) 。从这些模式的名称中就可以看出,它们都说了与
Model 相关的内容,但我意识到,在大多数情况下,我的应用中根本不存在 Model 。
举个例子:从后端获取并显示一个用户列表。一般 MVP 的实现方式应该是这样的:
class PersonsPresenter extends Presenter {
public void load() {
getView().showLoading(true); // 显示一个进度条
backend.loadPersons(new Callback() {
public void onSuccess(List persons) {
getView().showPersons(persons); // 展示用户列表
}
public void onError(Throwable error) {
getView().showError(error); // 展示错误信息
}
});
}
}
Model 在哪里?哪个是 Model ?backend ?不,那是业务逻辑。Person 列表?不,那只是用于展示的信息之一,与那个进度条、错误信息没什么不同。那么,哪个是真正的 Model ?
在我想来,至少也该有个 Model 类:
class PersonsModel {
// 在实际应用中,这些字段应当是 private
// 并且生成 getter
final boolean loading;
final List persons;
final Throwable error;
public PersonsModel(boolean loading, List persons, Throwable error) {
this.loading = loading;
this.persons = persons;
this.error = error;
}
}
然后, Presenter 应当这么实现:
class PersonsPresenter extends Presenter {
public void load() {
getView().render(new PersonsModel(true, null, null)); // 显示一个进度条
backend.loadPersons(new Callback() {
public void onSuccess(List persons) {
getView().render(new PersonsModel(false, persons, null)); // 展示用户列表
}
public void onError(Throwable error) {
getView().render(new PersonsModel(false, null, error)); // 展示错误信息
}
})
}
}
现在,就有了一个可以 渲染 (render) 到屏幕上的 Model 了。这并不是什么新概念,Trygve Reenskaug 在1979年定义的初版 MVC 就有类似的概念:View 关注 Model 的变化,并随之更改。然而,现在的 MVC 已经涵盖了太多不同的设计模式(多是谬用),与 Reenskaug 当初的构想南辕北辙。比如,后端程序员使用
MVC 架构的框架,iOS 也有 ViewConrtoller ,那么,Android 平台 MVC 的真正含义是什么呢?Activities 是 Controller ?那 ClickListener 又是什么?现在所说的 MVC 就是对当初 Reenskaug 创造的那个 MVC 的错误(mistake)、错用(misusage)、错解(misinterpretation)!那就此停止对 MVC 的探讨吧,再说下去就跑题了。
回到我最开始说的话题上,一个 Model 可以解决许多在 Android 开发中遇到的难以解决的问题。
通过这些点,我们来看看传统的 MVP 、 MVVM 是如何解决这些问题的,而用上 Model ,它又是如何帮助我们避免一些常见陷阱的。
状态问题
响应式应用——这是一种比较新鲜的说法。具体来说就是,一个拥有 UI 的应用对状态的改变做出响应。嗯,好吧,又多出了一个词语,状态(state) 。状态是什么?通常来说,我们把我们在屏幕上看到的称作“状态”,比如,当界面上显示一个进度条的时候,就可以称它为“加载状态”。个中关键,我们前端工程师往往更加关注 UI。这并不是一件坏事,一个好的 UI 决定了是否会有用户来使用我们的应用,并且这关乎到一个应用能够获取多大的成功。但是,看看上面那段 MVP 代码(没有使用 PersonsModel 的那个)吧,UI 的状态居然是由 Presenter 来决定的,因为是 Presenter 告诉 View 该显示什么的。MVVM 也一样。在这篇文章里,我想要讨论一下两种 MVVM 的实现方式:第一种使用 Android 的 Data Binding ,第二种使用 RxJava 。在使用了 Data Binding 的 MVVM 中,状态可以直接放置在 ViewMode 中:
class PersonsViewModel {
ObservableBoolean loading;
// ... 为了更好的阅读,我忽略了其他的字段
public void load() {
loading.set(true);
backend.loadPersons(new Callback() {
public void onSuccess(List persons) {
loading.set(false);
// ... 其他处理,比如设置用户列表
}
public void onError(Throwable error) {
loading.set(false);
// ... 其他处理,比如设置错误信息
}
});
}
}
使用 RxJava 的 MVVM ,我们没有使用数据绑定引擎,但也在 View 中将 Observable 绑定到了 UI 组件上:
class RxPersonsViewModel {
private PublishSubject loaidng;
private PublishSubject> persons;
private PublishSubject loadPersonsCommand;
public RxPersonsViewModel() {
loadPersonsCommand.flatMap(ignored -> backend.loadPersons())
.doOnSubscribe(ignored -> loading.onNext(true))
.doOnTerminate(ignored -> loading.onNext(false))
.subscribe(persons);
// 也可以使用其他的实现方式
}
// 在View中注册(i.e. Activity / Fragment)
public Observable loading() {
return loading;
}
// 在View中注册(i.e. Activity / Fragment)
public Observable> persons() {
return persons;
}
// 当触发这个动作(调用它的 onNext())时加载用户列表
public PublishSubject loadPersonsCommand() {
return loadPersonsCommand;
}
}
当然,这些代码片段并不完美,你的实现也许和上面完全不一样,重点是,在 MVP 和 MVVM 中,状态通常是由 Presenter 或者 ViewModel 来驱动的。这就导致了下面的问题:
- 业务逻辑有自己的状态,
Presenter(或者ViewModel)有自己的状态(你可能会试着同步业务逻辑和Presenter的状态),View也可能有自己的状态(比如,出于某种需要,你可能在View中直接设置visibility,或者,在屏幕旋转时 Andriod 本身也会往bundle中存储状态)。 Presenter(或者ViewModel)可以有多个输入(View触发由Presenter提供的action),这没有什么问题,但Presenter也有多个输出(或者多个输出通道,像MVP/MVVM提供的多个Obervable用来表示view.showLoading()或者view.showError()),这就会导致View、Presenter与业务逻辑的状态冲突,多个线程时尤其严重。
在最好的情况下,这仅仅会导致一些可以看到的 bug,比如,同时显示了加载框(加载状态)和错误框(错误状态)。(译者注:此处有视频演示,但是是 Youtube 的,自己原文去找吧)
但在最糟糕的情况下,会有许多产生闪退的 bug ,你可能就需要用到诸如 Crashlytics 之类的工具去分析问题了。并且,这些代码很难重用,进而导致你的应用的问题难以修复。
那么,如果我们自底(业务逻辑)向上( View )只提供一个状态源呢?事实上,在这片文章一开始,当我们讨论 Model 的时候,我们就已经看见了相似的概念了。
class PersonsModel {
// 在实际应用中,这些字段应当是 private
// 并且生成 getter
final boolean loading;
final List persons;
final Throwable error;
public PersonsModel(boolean loading, List persons, Throwable error) {
this.loading = loading;
this.persons = persons;
this.error = error;
}
}
猜到了吗?Model 反映了 State 。 一意识到这一点,我就知道,许多和状态相关的问题都可以解决了(并且是从源头上解决的),由此,我的 Presenter 就只有了一个输出:getView().render(PersonsModel) 。这就像数学里的函数一样, f(x)= y (也可以有多个输入,一个输出, f(a, b,c)
)。可能不是每个人都对数学感冒,但是一个数学家不知道什么是 bug,而程序员知道。
理解什么是 Model ,怎样正确地去构建一个 Model ,这很重要,因为最终, Model 可以解决这个“状态问题”。
屏幕旋转
在 Android 开发中,屏幕旋转是一个很有挑战性的问题,最简单的解决方法就是忽略它,屏幕的方向改变了,重新加载所有的数据就行。这个方法很有效果,大多数情况下,你的应用是脱机工作,数据来自于本地数据库或者其它的本地缓存,因此,屏幕旋转时加载数据的速度是非常快的。然而,我个人十分讨厌看见加载框,即使只有几毫秒。我认为,这不是一个无缝的用户体验。所以,人们(包括我自己)开始使用 MVP 的 “retaining presenter”,尽管 View 在屏幕旋转的过程中会被分离销毁,但 Presenter 还在内存里,这样,View 就可以根据 Presenter 重建。使用 RxJava 的 MVVM 也有同样的概念,但我们得牢记,一旦 View 从它的 ViewModel 中 unsubscribe ,observable 流就被销毁了,你可以使用 Subjects 来避免这个问题。而使用了 Data Binding 的 MVVM ,ViewModel 是直接绑定在 View 上的,为了避免内存泄露,在屏幕旋转时你必须销毁它。
但 “retaining presenter” 的问题是:我们怎样确保旋转屏幕前后,View 与 Presenter 的状态一致呢?我写过一个 MVP 库 Mosby ,其中有个特性,ViewState , 实现了 View 与业务逻辑的同步。
Moxy ,另一个 MVP 库,用一种相当有趣的方式——“命令”,来解决屏幕旋转后 View 的状态恢复问题:
可以肯定,还有其他解决 View 状态的方法。回过头,让我们总结一下那些库试图解决的问题:它们试图解决我们讨论的有关状态的问题。
所以,再次强调,一个 Model 反映当前的 “状态”,一个方法用于 “渲染” 这个 Model ,这样,通过简简单单地调用 getView().render(PersonsModel) 就可以解决所有问题了(通过最新的 Model ,将 View 重新添加到 Presenter 上)。
返回栈的导航
一个 Presenter (或者 ViewModel )不再使用的时候,还需要维护它吗?举个例子,如果用户跳转页面,一个 Fragment (View)被另一个 Fragment 替换掉了,那么 Presenter 上就没有 View 了,很显然,Presenter 也就不能将最新的业务数据更新到
View 上了。那如果用户又返回了呢(比如按下返回按钮)?是重新加载数据还是重用存在的 Presenter ?这更多的是哲学问题。通常来说,一旦用户返回到上一屏(出栈),他肯定还是希望看到他离开时的样子。这就回到了上一个问题讨论的存储 View 的状态的问题了,那解决方案也很直接:一个 Model 表示状态,当从栈内返回时,我们只需要调用 getView().render(PersonsModel) 去渲染 View 就行了。
进程被系统杀死
在 Android 开发中,一种常见的误解是,进程被系统终止是一件很麻烦的事,我们不得不借助一些库来帮助我们存储状态(还包括 Presenter / ViewModel )。首先,进程只会因为一个原因而终止:Android 系统需要更多的资源,用以分配其他的应用,或者为了省电。当你的应用一直处于前台、用户正在操作的时候,这些都不可能发生。所以,好好做人,不要和操作系统怼。如果你真的有一些需要长时间运行在后台的工作,那就使用 Service ,这是告诉操作系统你的应用仍然在使用的唯一方案。当系统杀死一个进程的时候,会调用诸如 onSaveInstance() 之类的方法来保存状态。看清楚,是状态。我们应该把 View 相关的信息存入 Bundle 吗?Presenter 有它自己单独要存入 Bundle 的状态信息吗?业务逻辑的状态呢?我们已经解决了这些问题:正如前三条描述的那样,我们只需要一个
Model 类来表示整个状态。把这一个 Model 从 Bundle 中存入、取出都相当简单。但是,我个人认为,大多数情况下,像第一次启动应用那样去加载整个界面比保存状态要好。想象一个新闻阅读类的应用,它用来展示一些最新鲜的文章,当应用关闭的时候,我们保存了状态,用户下次打开的时候(比如六小时后)重新载入状态,那用户看到的就会是过时的内容了。在这样的场景下,保存 Model 和状态,还不如简单地重新加载数据。
不可变、单向的数据流
我不打算细说 不可变性 的优越性,关于这点已经有了太多资源了。我们就是想要一个不可变的 Model (它代表了状态)。为什么?因为我们希望只有一条数据源。当传输 Model 的时候,我们也不希望应用中的其他组件对它进行修改。现在假设,我们要写一个简单的计数软件,它有一个增加按钮和一个减少按钮,计数值放在一个
TextView 里。如果我们的 Model (在这个例子里,它就是计数的值,一个 int )是不可变的,那我们该怎么去计数?我很期待你的答案。按钮按下的时候,我们不会直接去操作 TextView 。一些已知条件是:第一,我们的 View 应当只有一个 view.render(...) 方法;第二,我们的 Model 不可变,所以不要想着直接去更改它;第三,只有一个数据源——业务逻辑。那就用按钮按下去的动作模拟业务逻辑。业务逻辑知道当前的 Model (比如,它持有一个当前 Model 的私有变量),而随着增/减操作,它可以根据上一个 Model 新建一个 Model 。
这样,我们就根据业务逻辑(唯一的数据源)建立了一条单向的数据流,而且这里的 Model 是不可变的。但是,和一个简单的计数器相比,这显然过度设计了,不是吗?没错,计数器是一个简单的应用,不过大多数应用都是从简单开始,很快就变得复杂了。所以从我的角度来看,即使是一个简单的应用,一条单向的数据流和一个不可变的 Model 也很关键,这样才能确保,当应用的复杂度上升时,对开发者而言仍然有着相对简单的逻辑。
可调试、可重用的状态
除此之外,单向的数据流可以确保调试的简单。当我们下一次从 Crashlytics 中拿到 crash 信息的时候,我们就可以很轻松地重现、修复那些 bug ,因为所有必要的信息在 crash 报告中都可以找到。什么是“必要的信息”呢?我们所需要的全部信息就是当前的 Model 和 crash 发生时用户的操作(比如,点击减少按钮)。这就是我们重现 crash 所需要的信息,这很容易记录到日志里。如果不是单向的数据流(比如,有人用错了 EventBus ,CounterModel 不知道传到哪里去了),不是不可变性(否则,我们就很难确定是谁改变了 Model ),那就没有这么容易了。
可测试
传统的 MVP 和 MVVM 提升了应用的可测试性。MVC 当然也能测试:从来没人要我们把所有的业务逻辑放在 Activity 里面。用 Model 表示状态,我们可以简化我们单元测试的代码,只要简单地判断 assertEquals(expectedModel, model) ,这大大减少了我们需要模拟的数据。除此之外,许多验证性的代码也可以移除了,比如:Mockito.verify(view, times(1)).showFoo() 。最终,我们单元测试的代码就有了更高的可读性、可理解性,还有可维护性,因为我们不再需要去关注、解决真实代码中的实现细节。
总结
这一系列的第一篇文章,我们讨论了许多理论上的东西。我们真的需要为 Model 专门写一篇文章吗?众所周知,Model 很重要,它可以帮助我们防止一些难缠的问题。Model 并不等同于业务逻辑,而是业务逻辑(比如,Interactor ,Usecase ,Repositor 或者你应用里其它叫法(译者注:此处应该指 B(usiness)O(bject)
到 Model 的转换器))产生了 Model 。另一方面,我们会在实战中重新理解这些关于 Model 的理论,没错,我们最终会用 MVI 的模式构建一个响应式的应用。我们要写的 demo 是一个虚拟的在线商店。下面这个 视频是第二部分会涉及到的内容,期待吧~