专栏首页曲水流觞TechRillMVVM、RxJava、Retrofit三剑合璧,事半功倍

MVVM、RxJava、Retrofit三剑合璧,事半功倍

前言

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(geniusiandev)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2018-07-27

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • DàYé玩转数据战略Step By Step

    我们先看看工业革命的演进路径,从1.0的蒸汽机时代,到2.0的电力、流水线和大规模生产时代,再到3.0的计算机自动化时代,最后是4.0的智能化时代。

    曲水流觞
  • 不要再被MYSQL的INT(M)给迷惑了

    MYSQL里有些比较隐晦的知识点,经常被稀里糊涂的用错而不自知。以前评审过程发生的次数较多,索性随手写一些出来。前人栽个树,后人一定要乘凉啊!

    曲水流觞
  • 【致38节】消除代码里的“性别偏见”

    众所周知,编程女性实乃凤毛麟角,不说职场偏见,单说一些程序设计都会流露出“性别偏见”,打引号是因为接下来我说的纯属玩笑,大家可不要当真。

    曲水流觞
  • CVPR 2019 Oral | 视觉识别新范式:开放世界下的大规模长尾识别

    上周一篇发布于arXiv的CVPR 2019 Oral论文引起了广泛的关注,来自香港中文大学与加州大学伯克利分校的研究学者重新思考了真实世界的视觉识别类别分布的...

    CV君
  • 程序员瞎长类动物,这个视频肯定笑死你,但是安全不能瞎

    http://mpvideo.qpic.cn/tjg_3081972243_50000_e5e57902e8ad407f90be3cafafe5328b.f10...

    java乐园
  • leetcode题解 | 48. 旋转图像

    你必须在原地旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要使用另一个矩阵来旋转图像。

    ACM算法日常
  • 速收藏!最强数据集50个最佳机器学习公共数据!

    https://medium.com/datadriveninvestor/the-50-best-public-datasets-for-machine-le...

    zenRRan
  • 最强数据集50个最佳机器学习公共数据,可以帮你验证idea!

    外国自媒体mlmemoirs根据github、福布斯、CMU官网等信息,整理了一张50个最佳机器学习公共数据集的榜单,量子位为大家分享一下~

    昱良
  • 撕掉标签,看小姐姐如何撑起数据科学的“半边天” | 数据科学50人洞见

    在数据科学领域,女性从业者的身影仍然很少。为了更加深入地了解这一群体,DT数据侠联合猎聘大数据研究院、科技女性社群LWT发起了数据科学领域女性从业者的调研。

    DT数据侠
  • 资源 | 机器学习高质量数据集大合辑

    在机器学习中,寻找数据集也是非常重要的一步。质量高或者相关性高的数据集对模型的训练是非常有帮助的。

    大数据文摘

扫码关注云+社区

领取腾讯云代金券