前言
MVVM和MVP是当前比较流行的两种应用开发架构,两者各有优缺点。目前公司所有Android项目用的都是MVP架构,所以这篇文章我们可以一起来深入了解一下。当然光讲MVVM显得有些空洞,因此本文还会附带上了解经常和它搭配使用的RxJava和Retrofit。
说明:MVVM是一种基于数据驱动的架构思想,在很多平台的开发中都有应用,比如ReactNative、小程序等,RxJava是一套基于操作流的异步框架,同样对很多平台都有支持。本文仅讨论在Android平台应用开发中的使用,其它平台也相似但可能存在些许差别。
简介
在搭建MVVM项目之前我们先来简单了解下MVVM模式,以及和MVP的区别。
MVVM的全称就是Model、View、ViewModel。View:视图,对应Activity和xml文件,纯粹的ui展示层,不涉及任何的业务流程;Model:数据模型,我们就可以理解成数据提供方;ViewModel:最重要的一层结构,从名字就可以看出,它是连接View和Model的桥梁,它会把数据更新到ui上,也会接受来自ui的交互事件,并处理相应的业务。
说起更新ui的操作,和以前不同的是,它不会获取到控件之后调用控件的方法实现更新ui,而是依赖DataBinding来实现双向绑定。Databinding 是一种框架,MVVM是一种模式,两者的概念是不一样的。DataBinding实现了控件和数据的绑定关系,是实现MVVM模式的工具,这套机制可以实现ui控件和数据之间的动态监听和动态更新。值得庆幸的是,DataBinding框架Google已经帮我们做好了。在Android中我们可以通过一个简单的配置集成DataBinding,使我们构建Android MVVM 应用程序成为可能。
MVVM和MVP相比不能说哪个好哪个不好,只能说各有特点和优劣,下面简单列举一些MVVM的优势。
数据驱动
为什么这点放在第一条,因为这是MVVM最大的特点。在传统开发模式中,必须先处理业务数据,然后根据数据变化,去获取ui控件的引用再调用控件方法更新ui,同时通过监听ui控件来获取用户输入。而在MVVM中,数据和业务逻辑处于一个独立的ViewModel中,ViewModel只要关注数据和业务逻辑,不需要和ui控件打交道。由数据自动去驱动ui控件自动更新,ui控件的改变又会自动反馈到数据,数据成为主导因素。这样就使得在业务逻辑处理只需要关心数据,方便而且简单很多。
低耦合度
MVVM模式中,数据是独立于ui的,ViewModel只负责处理和提供数据,ui想怎么展示数据都由控件自己决定,ViewModel 不涉及任何和ui控件相关的事也不持有ui控件的引用。即使控件改变(TextView 换成 EditText),ViewModel也几乎不需要更改任何代码,专注自己的数据处理就可以了,这一点和以前完全不同。如果是MVP遇到ui更改,就可能需要改变获取控件的方式,改变更新ui的接口,改变从ui上获取输入的代码,可能还需要更改访问控件对象的属性代码等等,会非常麻烦。因此,MVVM还极大地降低了两者的耦合度。
更新 UI
在MVVM中,我们可以在工作线程中直接修改ViewModel的数据(只要数据是线程安全的),剩下的数据绑定框架帮你搞定,很多事情都不需要你去关心。这一点太难得了,我们不需要再写Handler辅助更新ui。
团队协作
MVVM的分工是非常明显的,由于View和ViewModel之间是松散耦合的。一个是处理业务和数据,一个是专门的ui展示,完全可以由两个人分工来做。一个转做ui(xml 和 Activity)一个转写ViewModel业务,效率更高(尽管实际开发我们不会这么分工,但是MVVM确实可以做到这一点)。
可复用性
一个ViewModel复用到多个View中,同样的一份数据,用不同的UI去做展示,对于频繁的版本迭代ui改动,只要更换View层就行。
实战Demo
在学习一个新东西时不能只看理论,既枯燥也容易忘,所以我们可以写一个小demo演练一下。
我们一起来写一个简单的demo,类似社交软件。包含四个页面:用户信息、好友列表、聊天记录、粉丝列表。
之所以设计这四个页面,是为了练习数据绑定的写法:
(1)用户信息:这个页面很简单,主要是演示常规页面数据绑定的写法
(2)好友列表:主要演示RecyclerView列表页面数据绑定的写法
(3)聊天页面:主要演示多类型RecyclerView列表Adapter的写法
(4)粉丝列表:从网络获取数据,主要演示RxJava+Retrofit的基本使用方法
构建
废话讲了这么多,下面开始正式搭建MVVM应用。我们先从最简单的页面—“用户信息”开始。
Gradle
UserInfoActivity
这里和传统写法不太一样,不是直接调用Activity的setContentView方法,而是调用DataBindingUtil.setContentView。这个方法返回一个ActivityUserInfoBinding对象,第一次接触DataBinding肯定很奇怪,这个类是哪来的呢?不要慌,这个类是自动编译生成的,根据layout.xml文件的名字,把下横线去掉,然后首字母大写,最后在加上一个Binding。比如我们传的layout名字叫:activity_user_info,根据规则生成的类名就叫ActivityUserInfoBinding。规则有点复杂,但是就是这么规定的。
Activity_user_info.xml
看完activity代码自然要看看下layout的代码。这段代码有点长,我们先不要看具体的代码,先看结构。
和我们通常的xml布局不一样,最外层是一个layout节点,然后包含两个子节点,第一个是data,第二个是LinearLayout。
这个结构是固定的,LinearLayout标签是我们正常的布局节点,我们可以像以前那样编写,此处就不细说了。Data标签是数据绑定的节点,用于指定该布局文件中需要用到的数据(ViewModel),Type是ViewModel类的全路径,Name是给ViewModel取的变量名,后面布局中就是用这个name访问ViewModel的数据。
在这里例子中,我们页面中展示的是用户信息,对应的ViewModel就是UserInfoViewModel,我们给它取个别名叫UserInfoViewModel,下面在访问用户名等就用UserInfoViewModel.name。
UserInfoViewModel
上文终于说到了ViewModel,我们一直说ViewModel是MVVM的核心,下面就来看看ViewModel的代码是怎么写的。
ViewModel是MVVM里最复杂的一层。首先是类名,有一个泛型<UserInfoActivity>,构造函数传入实例,这个不是必须的,是我自己在父类定义的。因为我们有可能在ViewModel中需要调用Activity的方法,比如跳转页面,所以需要有一个入口获取Activity实例。
再看成员变量。这些成员变量都是ObservableField类型或者ObservableInt类型,泛型包着的才是layout需要的数据, 为什么要用Observable包一下呢,因为需要自动更新ui。我们在layout文件中把ViewModel和控件绑定到了一起,当ViewModel的数据发生改变(比如:name.set("朱小明");)DataBinding就会自动把新的数据更新到ui上。
这么看ViewModel也不复杂,还有一些在这个简单的例子没有体现,比如点击事件,MVVM的点击事件是在layout.xml中定义:
看onClick属性,写法很奇怪,@{userInfoViewModel::onNameClick},中间是双冒号,这也是固定写法,onNameClick是ViewModel中的方法名:
另外在这里LoadUserInfo方法写的是假数据,真实项目中这里肯定是请求网络数据,这在最后粉丝列表页面中也会讲到。
这样,一个最最简单的页面就完成了。
RecyclerView
完成了最简单的页面,我们来看看MVVM的列表页面怎么实现。首先是layout.xml。
看完第一个例子,再看这个布局就很简单了,大结构都一样,只不过这里的主布局是RecyclerView,绑定的ViewModel是FriendListViewModel,再看下Activity里是怎么设置RecyclerView的。
FriendListActivity
我们惊讶地发现FriendListBinding居然可以直接访问RecyclerView对象。其实和前面的setViewModel方法一样,这也是根据xml中控件名自动编译生成的。拿到了RecyclerView实例就可以设置LayoutManager和Decoration,这都没什么说的。
(PS:这里友情提示一下,这些编译生成的代码是在你Coding过程中动态产生的,所以有时候你会遇到你代码写完了,但是它没有编译出来的情况,不要惊慌,手动Build一下就好。)
紧接着就是设置Adapter,这里值得说的是:我在这里用的是CommonAdapter,那么它是怎么实现的呢?
CommonAdapter
用MVVM的方式写Adapter和以前的写法是不一样的,看下完整代码:
代码有点多,如果仅仅是写一个普通的Adapter,其实并不需要这么复杂,我是想让它通用一点所以加了泛型。仔细看代码,和传统的Adapter在大体结构上是一致的,不同的主要是两点:
(1)加载布局 onCreateViewHolder()
(2)绑定数据 onBindViewHolder()
加载布局时,我们要用DataBindingUtil.inflate()方法,该方法返回一个ViewDataBinding对象,然后把传递给ViewHolder,传统的写法ViewHolder持有的是一个View,而这里是一个ViewDataBinding。
绑定数据时,不同于以前直接给控件赋值的方式,而是调用了ViewDataBinding的setVariable(mVariableId, itemInfo)和executePendingBindings()方法,这种方式和前面的例子是一样的,都是把View和ViewModel绑定在一起,只不过这种写法比较手工。
mVariableId是什么呢?它其实是xml中申明的ViewModel的ID。比如我们在xml中申明了一个ViewModel,Name叫FriendListViewModel,就会自动在BR类中编译出一个ID,叫BR.friendViewModel,Activity中在New CommonAdapter时,就是传递的这个值。
FriendListViewModel
又到了ViewModel,我们看下列表的ViewModel是怎么写的:
这个ViewModel只有一个成员变量,仍然是ObservableField类型,泛型是List<FriendViewModel>,这个FriendViewModel又是什么呢?它也是一个ViewModel,它是RecyclerView每一个item的ViewModel,item在加载时也和普通布局一样,也是通过绑定一个ViewModel来加载数据的。
看到这就明白了吧,这就是MVVM的风格,不管是普通页面、列表页面、还是列表元素,Layout.xml只管展示,但是Layout.xml必须绑定一个ViewModel,数据都来自ViewModel,ViewModel处理业务逻辑并通过DataBinding更新数据。
这里就不贴FriendViewModel的代码了,聪明的你一定知道该怎么写了。我们一起看下运行效果图:
多类型RecyclerView
按照传统的写法,我们要写一个多类型的Adapter和单类型的Adapter是基本一样的,只是为不同类型加载不同布局,然后再为不同类型的布局分别赋值。在MVVM中也是如此,只不过我们要为每一种Type匹配一个Layout.xml,再为每一个Layout.xml匹配一个VariableId用于绑定数据。先看下Adapter的全部代码:
这么多代码不用全看,就看和之前单类型Adapter的区别,主要有一下三点:
(1)成员变量多了一个mLayoutMapping
(2)onCreateViewHolder()方法根据Type获取不同的Layout.xml
(3)onBindViewHolder()方法为不同的Layout.xml匹配不同的VariableId,绑定不同的数据
mLayoutMapping保存Type类型和Layout.xml的对应关系,因为在onCreateViewHolder()要根据Type获取Layout,为什么不把Layout的ID放在T类型的Bean里面呢,因为onCreateViewHolder()的参数里只有Type,没有Position, 我们没有办法获取到每个位置的Bean。而且多类型Adapter的Type和Layout的对应关系本来就不应该和Bean相关,它就是独立的一组对应关系,所以用SparseIntArray保存起来。
onBindViewHolder()方法中,会为每一个Layout.xml绑定一个VariableId(就是ViewModel的id),而这个VariableId是从每个T类型Bean里取出来的,这里为什么放在Bean里,因为这里有Position参数,我们可以获取到每个位置的Bean。当然这个VariableId是通过一个方法获取的,这个方法定义在父类,子类重写并返回对应的VariableId。
这里每一条消息的ViewModel是MessageViewModel,它继承自MultiTypeListItemViewModel,看下代码:
MultiTypeListItemViewModel.java
MessageViewModel.java
父类里有一个mType成员变量,还有一个VariableId()抽象方法,在构建子类对象时给mType赋值,并在子类中重写VariableId()方法,返回每种Type对应的VariableId。
但是我这里为什么我只返回固定的VariableId呢?因为我的聊天界面左右Type的Layout所对应的ViewModel 是一样的,如果在你的项目中是不一样的,那你就需要返回每种Type对应的VariableId。
这样我们就完成了多类型的列表页面:
RxJava+Retrofit网络请求
前面讲的三个页面都是单机游戏,数据都是本地假数据,最后来看下如果用RxJava+Retrofit进行网络请求,从服务器请求粉丝列表。
当然这个粉丝不是真的啦,只是我自己搭建的一个本地服务器,返回一段固定的Json,能起到演示效果就好。
http://99.48.58.51:8080/springMvcDemo/testController/testFansList.do
(PS:这里用到的头像取自多位Android博客大神的主页,向大神致敬!)
(PS:RxJava和Retrofit的使用细节不在这篇文章里讲,如果想了解详情可以参考其它博客https://www.jianshu.com/u/26ef80e64974)
下面开始正式写代码。
Gradle
Service
Retrofit的每一个请求都需要先定义一个Service,这个Service是一个Interface,里面定义每一个请求的方法。
然后调用RetrofitManager的Create方法创建Service实例。
至于它是怎么创建实例的先不用管,反正我们拿到了这个Service实例就可以调用它的方法了。
Service的RequestFansList()方法返回的是Observable对象,所以Map函数把它转换成我们需要的结果数据List<FansBean>。
Compose指定请求网络和结果回调的线程。
PreAction主要是为了在请求之前做一些前置操作,比如ShowLoadingView啥的。
Subscribe绑定了订阅者,下面就来看下这个订阅这做了哪些事情。
onCompleted()表示事件序列的结束,所以我们需要finishLoadingView。
onError()表示事件序列过程中发生异常,它和onCompleted()是互斥的,只会走其一,我们需要在这个回调中做相应的异常处理,并finishLoadingView。
onNext()就是事件的正常返回了,我们得到请求结果后,转换成ViewModel的数据,就可以更新ui了。
(PS:这里必须再次说明下,网络请求这块我省略了很多零碎的东西,因为这篇文章主讲MVVM,不是主讲Retrofit,一些关于Retrofit使用、配置的细节没有一一列出,如果有不清楚的地方还请查阅Retrofit相关文档。)
@BindingAdapter
文章的最后,必须要说一下
@BindingAdapter,看了前面的代码,大家心中可能会有以下一些疑惑:
ImageView
为什么ImageView设置三个Img参数就能加载图片?
RecyclerView
为什么RecyclerView这样设置Data就可以加载出数据?
答案就是:其实这背后都是@BindingAdapter这个注解在工作。
我们随便写一个类,真的是随便写,因为类名不重要。然后写一个静态方法用于加载图片。 这个方法上加上一个注解@BindingAdapter({"img:imgurl", "img:placeholder", "img:error"}),注解里面有三个参数,分别表示图片url,占位图,错误图。
方法的参数是需要加载数据的控件,和注解里申明的三个参数,然后在方法内部我们可以用自己喜欢的方式加载图片,我用的是Glide,你也可以用Picasso。这样我们就定义好了一个@BindingAdapter。
在Xml中使用时,我们首先要申明命名空间
这个命名空间Img就是在注解里定义的Img,必须保持统一。否则报错。
然后就用这个命名空间给控件赋值。
还有一点很重要的是,注解里申明了几个参数,就必须传几个参数否则会报错。
ImageView的说完了,RecyclerView自然就简单了。
第一个BindingAdapter是用于单类型RecyclerView,第二个例子就是用的这个。
第二个BindingAdapter是用于多类型RecyclerView,第三个例子用的就是这个。
另外还有一点需要提下,如果一个控件的某个属性没有Set方法,也是需要用这种方式写的。如果是自定义View的属性,也是可以用这种方式写的。
总结
好吧,以上就是本次分享的全部。啰啰嗦嗦讲了这么多,思绪也许整理得不是很好,但是MVVM+RxJava+Retrofit的使用确实也比较复杂,这篇文章算是一个入门。如有错误或不当之处欢迎留言指出。
任家亮
享米Android开发工程师,高冷的葬爱家族大公爵。摩羯男一枚,最大的爱好是王者荣耀,看科探片。
本文分享自 曲水流觞TechRill 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!