Android MVX系列开发模式
MVC(Model-View-Controller)
MVC模式分为三部分:
- 视图(View):用户界面,它会接收用于的交互请求并展示数据信息给用户。
- 控制器(Controller):主要担任Model与View之间的桥梁,用于控制程序的流程。Contrller负责确保View可以访问到需要显示的Model对象数据,并充当View了解Model更改的渠道。View接收到用户的交互请求之后,会将请求转发给Contrller,Contrller解析用户的请求之后,会交给对应的Model去处理。因此,理论上,Contrller应该是「很轻的」。
- 模型(Model):主要管理业务模型的数据和行为,它即保存程序的数据,也定义了处理数据的逻辑。
各部分之间的通信方式如下:
- 所有的通信都是单向的
- View传送指令到Controller
- Controller完成业务逻辑后,要求Model改变状态
- Model将新的数据发送到View,用户得到反馈
互动模式 接受用户指令时,MVC可以分为两种方式。一种是通过View接受指令,传递给Controller;另一种是直接通过Controller接受指令。
MVP (Model-View-Presenter)
MVP 的本质:是广义上的架构模式,适用于面向实体或虚拟用户接口的开发。它主要是在 MVC 的背景下,通过依赖倒置,来解决「逻辑复用难」 以及「实现替换难」的问题。
MVP里他通常包含的4个要素:
- View:负责绘制UI元素、与用户进行交互(在Android中体现为Activity)
- View Interface:需要View实现的接口,View通过View interface与Presenter进行交互,降低耦合,方便进行单元测试
- Model:负责存储、检索、操纵数据(有时也实现一个Model interface用来降低耦合)
- Presenter:作为View与Model交互的中间纽带,处理与用户交互的负责逻辑
MVP模式将Controller改名为Presenter,同时改变了通信方向。
- 各部分之间的通信,都是双向的
- View和Model不发生联系,都通过Presenter传递
- View非常薄,不部署任何业务逻辑,称为”被动视图”(Passive View),即没有任何主动性,而Presenter非常厚,所有逻辑都部署在那里。
MVP 的优点
- 降低耦合度,实现了Model和View真正的完全分离,可以修改View而不影响Modle
- 模块职责划分明显,层次清晰
- 隐藏数据
- Presenter可以复用,一个Presenter可以用于多个View,而不需要更改Presenter的逻辑(当然是在View的改动不影响业务逻辑的前提下)
- 利于测试驱动开发。以前的Android开发是难以进行单元测试的(虽然很多Android开发者都没有写过测试用例,但是随着项目变得越来越复杂,没有测试是很难保证软件质量的;而且近几年来Android上的测试框架已经有了长足的发展——开始写测试用例吧),在使用MVP的项目中Presenter对View是通过接口进行,在对Presenter进行不依赖UI环境的单元测试的时候。可以通过Mock一个View对象,这个对象只需要实现了View的接口即可。然后依赖注入到Presenter中,单元测试的时候就可以完整的测试Presenter应用逻辑的正确性。
- View可以进行组件化。在MVP当中,View不依赖Model。这样就可以让View从特定的业务场景中脱离出来,可以说View可以做到对业务完全无知。它只需要提供一系列接口提供给上层操作。这样就可以做到高度可复用的View组件。
- 代码灵活性
MVP 的缺点
- 由于V/P之间的互相耦合,从代码分层角度来说,层之间未做到单向引用;无法做到P层的业务复用。
- 不符合单一职责原则,由于V/P是一对一的,如果业务很复杂的话,P会承担大量的责任。
- 声明周期不易于管理,实际上大部分App并不想需要处理转屏等复杂应用场景,但即使这样,我们经常要关注页面关闭后,运行中的网络请求是否需要停止,是否会造成空指针,甚至内存泄露。
- 不利于单元测试,一般情况下,QA写的单元测试case是针对于业务逻辑的,但是如果没有独立的业务逻辑层,是非常不利于实施的。
- 编码风格无法统一,如果编码风格得不到统一,每个人在做业务需求,或帮助其他人调试代码,亦或进行 code review 的时候,会非常困难,这时候一套能够让每个人都写成风格相似代码的框架显得尤为重要。
MVP 内存泄露
- P层的耗时任务再页面销毁时是否执行很关键,假设当页面销毁时,P层内的任务执行完,由于P层没有再被内部类等持有引用,所以P层是会被回收的,那V层也不被P层持有引用,所以即使没在V层销毁时清空软引用和置空(V层),V层同样会被销毁,不存在内存泄露问题。
- V层是否被P层弱引用持有决定V层是否会内存泄露,假设当页面销毁时,P层内的任务再执行,由于V层是被P层弱引用持有,所以V层是会被GC回收的,而P层由于任务还在执行,所以回收不了。
MVVM(Model-View-ViewModel)
MVVM 的本质:是狭义上的架构模式,专用于页面开发。
它主要是在多人协作的软件工程的背景下,通过只操作 ViewModel 中映射的视图数据来刷新视图状态,以此来解决视图调用的一致性问题从而规避不可预期的错误。
MVVM模式将Presenter改名为ViewModel,基本上与MVP模式完全一致,唯一的区别是,它采用Data-Binding
双向绑定:View的变动,自动反映在ViewModel,反之亦然。
数据绑定
MVVM 中最重要的一个特性就是数据绑定,通过将View的属性绑定到ViewModel,可以使两者之间松耦合,也完全不需要在ViewModel里写代码去直接更新一个View。数据绑定系统还支持输入验证,这将提供了将验证错误传输到View的标准化方法。
通过数据绑定,当 ViewModel 的数据发生改变之后,与之绑定的 View 也会随之自动更新。反过来,如果 View 发生了变化,那 ViewModel 是否也同样会随之变化呢?这就涉及到数据绑定的两种类型:
- 单向绑定:ViewModel 与 View 绑定之后,ViewModel 变化后,View 会自动更新,但反之不然,即数据传递的方向是单向的。(ViewModel —> View)
- 双向绑定:ViewModel 与 View 绑定之后,如果 View 和 ViewModel 中的任何一方变化后,另一方都会自动更新,这就是双向绑定。(Model <—> View)
一般情况下,在视图中只显示而无需编辑的数据用单向绑定,需要编辑的数据才用双向绑定。
前面我们已经了解到,ViewModel 封装的数据包含 View 的属性和命令两种,因此,数据绑定其实也可分为属性绑定和命令绑定。比如,TextView 的内容绑定的就是属性,Button 的点击事件绑定的就是命令。
MVI
表现层逻辑(Presenter/ViewModel)
为什么要让Presenter/ViewModel处理几乎所有的表现层逻辑?主要是为了提高可测试性,将尽可能多的表现层逻辑纳入到单元测试的范围中。因为对视图控件的显示等等进行单元测试太难了,所以View是基本上没发进行单元测试的。但是Presenter/ViewModel完全可以进行单元测试:
class ProfilePresenterTest {
private val presenter: ProfilePresenter
private val view: ProfileView
@Test
fun testShowEditStateOnBtnClick() {
// 浏览状态下点击编辑按钮,验证View是否显示了编辑状态视图
// 也就是验证view.showEditState()方法是否被调用了
presenter.setState(State.NORMAL)
presenter.onEditStateButtonClicked()
Mockito.verify(view).showEditState()
}
@Test
fun testShowNormalStateOnBtnClick() {
// 编辑状态下点击完成按钮,验证View是否显示了浏览状态视图
// 也就是验证view.showNormalState()方法是否被调用了
presenter.setState(State.EDIT)
presenter.onEditStateButtonClicked()
Mockito.verify(view).showNormalState()
}
}
业务层逻辑(Model)
静态的业务数据不能代表Model层,业务数据以及针对业务数据的操作功能构成了Model层,这也就是业务逻辑。Model层如何处理业务逻辑,来自于Presenter/ViewModel层的业务指令。
class RecommendBlogFeedPresenter {
private val view: RecommendBlogFeedView
private val model: BlogModel
fun onStart() {
view.showLoadWait()
model.loadRecommendBlogs { blogs: List<Blog> ->
view.showBlogs(blogs)
}
}
}
interface BlogModel {
void loadRecommendBlogs(callback: LoadCallback<List<Blog>>)
}
class BlogModelImpl : BlogModel {
private val repo: BlogFeedRepository
override fun loadRecommendBlogs { callback: LoadCallback<List<Blog>> ->
callback.onLoaded(repo.fetch("recommend"))
}
interface BlogFeedRepository {
fun fetch(tag: String): List<Blog>
}