内容简介:有朝一日,我突然发现我对于从结果上来说,最终我选择使用所谓的
有朝一日,我突然发现我对于 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 的MVP库,其包含一个名为 ViewState
的功能,它基本上将业务逻辑的状态与 View
同步。 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》系列目录
参考
[译]为什么使用MVI模式(MVI编写响应式安卓APP入门系列第一部分MODEL
关于我
Hello,我是 却把清梅嗅 ,如果您觉得文章对您有价值,欢迎 :heart:,也欢迎关注我的博客或者 Github 。
如果您觉得文章还差了那么点东西,也请通过 关注 督促我写出更好的文章——万一哪天我进步了呢?
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 一文读懂响应式编程到底是什么?(附赠国外优质 Java 学习视频)
- 理解响应者和响应链如何处理事件
- 从源码解析vue的响应式原理-响应式的整体流程
- Java程序员TCP 协议到底怎么学?学了到底有什么用?
- 到底什么是DeFi?
- 到底什么是“云原生”?
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
C语言名题精选百则技巧篇
冼镜光 / 机械工业出版社 / 2005-7 / 44.00元
《C语言名题精选百则》(技巧篇)收集了100则C语言程序设计题,共分9类。第一类比较简单,主要希望读者了解到《C语言名题精选百则》(技巧篇)的题目、解法与其他书籍之间的差异;第二至六类分别是关于数字、组合数学或离散数学、查找、排序、字符串等方面的题目;第七类列出了一些不太容易归类的题目,如Buffon丢针问题、Dijkstra的三色旗问题等;第八类则收录了一些有趣的、娱乐性的题目,如魔方阵等;第九......一起来看看 《C语言名题精选百则技巧篇》 这本书的介绍吧!
JS 压缩/解压工具
在线压缩/解压 JS 代码
图片转BASE64编码
在线图片转Base64编码工具