天天看点

Android MVVM 应用框架构建过程详解

说到android mvvm,相信大家都会想到google 2015年推出的databinding框架。然而两者的概念是不一样的,不能混为一谈。mvvm是一种架构模式,而databinding是一个实现数据和ui绑定的框架,是构建mvvm模式的一个工具。

之前看过很多关于android

mvvm的博客,但大多数提到的都是databinding的基本用法,很少有文章仔细讲解在android中是如何通过databinding去构建mvvm的应用框架的。view、viewmodel、model每一层的职责如何?它们之间联系怎样、分工如何、代码应该如何设计?这是我写这篇文章的初衷。

接下来,我们先来看看什么是mvvm,然后再一步一步来设计整个mvvm框架。

<a></a>

首先,我们先大致了解下android开发中常见的模式。

mvc

view:xml布局文件。

model:实体模型(数据的获取、存储、数据状态变化)。

controllor:对应于activity,处理数据、业务和ui。

mvp

view: 对应于activity和xml,负责view的绘制以及与用户的交互。

model: 依然是实体模型。

presenter: 负责完成view与model间的交互和业务逻辑。

前面我们说,activity充当了view和controller两个角色,mvp就能很好地解决这个问题,其核心理念是通过一个抽象的view接口(不是真正的view层)将presenter与真正的view层进行解耦。persenter持有该view接口,对该接口进行操作,而不是直接操作view层。这样就可以把视图操作和业务逻辑解耦,从而让activity成为真正的view层。

但mvp也存在一些弊端:

presenter(以下简称p)层与view(以下简称v)层是通过接口进行交互的,接口粒度不好控制。粒度太小,就会存在大量接口的情况,使代码太过碎版化;粒度太大,解耦效果不好。同时对于ui的输入和数据的变化,需要手动调用v层或者p层相关的接口,相对来说缺乏自动性、监听性。如果数据的变化能自动响应到ui、ui的输入能自动更新到数据,那该多好!

mvp是以ui为驱动的模型,更新ui都需要保证能获取到控件的引用,同时更新ui的时候要考虑当前是否是ui线程,也要考虑activity的生命周期(是否已经销毁等)。

mvp是以ui和事件为驱动的传统模型,数据都是被动地通过ui控件做展示,但是由于数据的时变性,我们更希望数据能转被动为主动,希望数据能更有活性,由数据来驱动ui。

v层与p层还是有一定的耦合度。一旦v层某个ui元素更改,那么对应的接口就必须得改,数据如何映射到ui上、事件监听接口这些都需要转变,牵一发而动全身。如果这一层也能解耦就更好了。

复杂的业务同时也可能会导致p层太大,代码臃肿的问题依然不能解决。

mvvm

view: 对应于activity和xml,负责view的绘制以及与用户交互。

model: 实体模型。

viewmodel: 负责完成view与model间的交互,负责业务逻辑。

mvvm的目标和思想与mvp类似,利用数据绑定(data binding)、依赖属性(dependency property)、命令(command)、路由事件(routed event)等新特性,打造了一个更加灵活高效的架构。

在常规的开发模式中,数据变化需要更新ui的时候,需要先获取ui控件的引用,然后再更新ui。获取用户的输入和操作也需要通过ui控件的引用。在mvvm中,这些都是通过数据驱动来自动完成的,数据变化后会自动更新ui,ui的改变也能自动反馈到数据层,数据成为主导因素。这样mvvm层在业务逻辑处理中只要关心数据,不需要直接和ui打交道,在业务处理过程中简单方便很多。

mvvm模式中,数据是独立于ui的。

数据和业务逻辑处于一个独立的viewmodel中,viewmodel只需要关注数据和业务逻辑,不需要和ui或者控件打交道。ui想怎么处理数据都由ui自己决定,viewmodel不涉及任何和ui相关的事,也不持有ui控件的引用。即便是控件改变了(比如:textview换成edittext),viewmodel也几乎不需要更改任何代码。它非常完美的解耦了view层和viewmodel,解决了上面我们所说的mvp的痛点。

更新ui

在mvvm中,数据发生变化后,我们在工作线程直接修改(在数据是线程安全的情况下)viewmodel的数据即可,不用再考虑要切到主线程更新ui了,这些事情相关框架都帮我们做了。

团队协作

mvvm的分工是非常明显的,由于view和viewmodel之间是松散耦合的:一个是处理业务和数据、一个是专门的ui处理。所以,完全由两个人分工来做,一个做ui(xml和activity)一个写viewmodel,效率更高。

可复用性

一个viewmodel可以复用到多个view中。同样的一份数据,可以提供给不同的ui去做展示。对于版本迭代中频繁的ui改动,更新或新增一套view即可。如果想在ui上做a/b testing,那mvvm是你不二选择。

单元测试

有些同学一看到单元测试,可能脑袋都大。是啊,写成一团浆糊的代码怎么可能做单元测试?如果你们以代码太烂无法写单元测试而逃避,那可真是不好的消息了。这时候,你需要mvvm来拯救。

我们前面说过了,viewmodel层做的事是数据处理和业务逻辑,view层中关注的是ui,两者完全没有依赖。不管是ui的单元测试还是业务逻辑的单元测试,都是低耦合的。在mvvm中数据是直接绑定到ui控件上的(部分数据是可以直接反映出ui上的内容),那么我们就可以直接通过修改绑定的数据源来间接做一些android

ui上的测试。

通过上面的简述以及模式的对比,我们可以发现mvvm的优势还是非常明显的。虽然目前android开发中可能真正在使用mvvm的很少,但是值得我们去做一些探讨和调研。

如何分工

构建mvvm框架首先要具体了解各个模块的分工。接下来我们来讲解view、viewmodel、model它们各自的职责所在。

view

viewmodel

viewmodel层做的事情刚好和view层相反,viewmodel只做和业务逻辑和业务数据相关的事,不做任何和ui相关的事情,viewmodel

层不会持有任何控件的引用,更不会在viewmodel中通过ui控件的引用去做更新ui的事情。viewmodel就是专注于业务的逻辑处理,做的事情也都只是对数据的操作(这些数据绑定在相应的控件上会自动去更改ui)。同时databinding框架已经支持双向绑定,让我们可以通过双向绑定获取view层反馈给viewmodel层的数据,并对这些数据上进行操作。关于对ui控件事件的处理,我们也希望能把这些事件处理绑定到控件上,并把这些事件的处理统一化,为此我们通过bindingadapter对一些常用的事件做了封装,把一个个事件封装成一个个command,对于每个事件我们用一个replycommand去处理就行了,replycommand会把你可能需要的数据带给你,这使得我们在viewmodel层处理事件的时候只需要关心处理数据就行了,具体见mvvm

light toolkit 使用指南的 command 部分。再强调一遍:viewmodel 不做和ui相关的事。

model

model层最大的特点是被赋予了数据获取的职责,与我们平常model层只定义实体对象的行为截然不同。实例中,数据的获取、存储、数据状态变化都是model层的任务。model包括实体模型(bean)、retrofit的service

,获取网络数据接口,本地存储(增删改查)接口,数据变化监听等。model提供数据获取接口供viewmodel调用,经数据转换和操作并最终映射绑定到view层某个ui元素的属性上。

关于协作,我们先来看下面的一张图:

Android MVVM 应用框架构建过程详解

上图反映了mvvm框架中各个模块的联系和数据流的走向,我们从每个模块一一拆分来看。那么我们重点就是下面的三个协作。

viewmodel与view的协作。

viewmodel与model的协作。

viewmodel与viewmodel的协作。

viewmodel与view的协作

Android MVVM 应用框架构建过程详解

图2中viewmodel和view是通过绑定的方式连接在一起的,绑定分成两种:一种是数据绑定,一种是命令绑定。数据的绑定databinding已经提供好了,简单地定义一些observablefield就能把数据和控件绑定在一起了(如textview的text属性),但是databinding框架提供的不够全面,比如说如何让一个url绑定到一个imageview,让这个imageview能自动去加载url指定的图片,如何把数据源和布局模板绑定到一个listview,让listview可以不需要去写adapter和viewholder相关的东西?这些就需要我们做一些工作和简单的封装。mvvm

light toolkit 已经帮我们做了一部分的工作,详情可以查看mvvm light toolkit

使用指南。关于事件绑定也是一样,mvvm light toolkit

做了简单的封装,对于每个事件我们用一个replycommand去处理就行了,replycommand会把可能需要的数据带给你,这样我们处理事件的时候也只关心处理数据就行了。

由图1中viewmodel的模块中我们可以看出viewmodel类下面一般包含下面5个部分:

context (上下文)

model (数据源 java bean)

data field (数据绑定)

command (命令绑定)

child viewmodel (子viewmodel)

我们先来看下示例代码,然后再一一讲解5个部分是干嘛用的:

context是干嘛用的呢,为什么每个viewmodel都最好需要持了一个context的引用呢?viewmodel不处理和ui相关的事也不操作控件,更不更新ui,那为什么要有context呢?原因主要有以下两点:

通过图1中,然后得到一个observable,其实这就是网络请求部分。其实这就是网络请求部分,做网络请求我们必须把retrofit

service返回的observable绑定到context的生命周期上,防止在请求回来时activity已经销毁等异常,其实这个context的目的就是把网络请求绑定到当前页面的生命周期中。

在图1中,我们可以看到两个viewmodel之间的联系是通过messenger来做,这个messenger是需要用到context,这个我们后续会讲解。

当然,除此以外,调用工具类、帮助类有时候需要context做为参数等也是原因之一。

model (数据源)

model是什么呢?其实就是数据源,可以简单理解是我们用json转过来的bean。viewmodel要把数据映射到ui中可能需要大量对model的数据拷贝和操作,拿model的字段去生成对应的observablefield然后绑定到ui(我们不会直接拿model的数据去做绑定展示),这里是有必要在一个viewmodel保留原始的model引用,这对于我们是非常有用的,因为可能用户的某些操作和输入需要我们去改变数据源,可能我们需要把一个bean在列表页点击后传给详情页,可能我们需要把这个model当做表单提交到服务器。这些都需要我们的viewmodel持有相应的model(数据源)。

data field(数据绑定)

data field就是需要绑定到控件上的observablefield字段,这是viewmodel的必需品,这个没有什么好说。但是这边有一个建议:

这些字段是可以稍微做一下分类和包裹的。比如说可能一些字段是绑定到控件的一些style属性上(如长度、颜色、大小),对于这类针对view

style的的字段可以声明一个viewstyle类包裹起来,这样整个代码逻辑会更清晰一些,不然viewmodel里面可能字段泛滥,不易管理和阅读性较差。而对于其他一些字段,比如说title、imageurl、name这些属于数据源类型的字段,这些字段也叫数据字段,是和业务数据和逻辑息息相关的,这些字段可以放在一块。

command(命令绑定)

command(命令绑定)简言之就是对事件的处理(下拉刷新、加载更多、点击、滑动等事件处理)。我们之前处理事件是拿到ui控件的引用,然后设置listener,这些listener其实就是command。但是考虑到在一个viewmodel写各种listener并不美观,可能实现一个listener就需要实现多个方法,但是我们可能只想要其中一个有用的方法实现就好了。更重要一点是实现一个listener可能需要写一些ui逻辑才能最终获取我们想要的。简单举个例子,比如你想要监听listview滑到最底部然后触发加载更多的事件,这时候就要在viewmodel里面写一个onscrolllistener,然后在里面的onscroll方法中做计算,计算什么时候listview滑动底部了。其实viewmodel的工作并不想去处理这些事件,它专注做的应该是业务逻辑和数据处理,如果有一个东西不需要你自己去计算是否滑到底部,而是在滑动底部自动触发一个command,同时把当前列表的总共的item数量返回给你,方便你通过

page=itemcount/limit+1去计算出应该请求服务器哪一页的数据那该多好啊。mvvm light toolkit

帮你实现了这一点:

接着在xml布局文件中通过bind:onloadmorecommand绑定上去就行了。

具体想了解更多请查看 mvvm light toolkit

使用指南,里面有比较详细地讲解command的使用。当然command并不是必须的,你完全可以依照自己的习惯和喜好在viewmodel写listener,不过使用command可以使viewmodel更简洁易读。你也可以自己定义更多的、其他功能的command,那么viewmodel的事件处理都是托管replycommand来处理,这样的代码看起来会比较美观和清晰。command只是对ui事件的一层隔离ui层的封装,在事件触发时把viewmodel层可能需要的数据传给viewmodel层,对事件的处理做了统一化,是否使用的话,还是看你个人喜好了。

child viewmodel(子viewmodel)

子viewmodel的概念就是在viewmodel里面嵌套其他的viewmodel,这种场景还是很常见的。比如说你一个activity里面有两个fragment,viewmodel是以业务划分的,两个fragment做的业务不一样,自然是由两个viewmodel来处理,这时候activity对应的viewmodel里面可能包含了两个fragment各自的viewmodel,这就是嵌套的子viewmodel。还有另外一种就是对于adapterview,如listview

它们的每个item其实就对应于一个viewmodel,然后在当前的viewmodel通过observablelist持有引用(如上述代码),这也是很常见的嵌套的子viewmodel。我们其实还建议,如果一个页面业务非常复杂,不要把所有逻辑都写在一个viewmodel,可以把页面做业务划分,把不同的业务放到不同的viewmodel,然后整合到一个总的viewmodel,这样做起来可以使我们的代码业务清晰、简短意赅,也方便后人的维护。

总的来说,viewmodel和view之前仅仅只有绑定的关系,view层需要的属性和事件处理都是在xml里面绑定好了,viewmodel层不会去操作ui,只是根据业务要求处理数据,这些数据自动映射到view层控件的属性上。

关于viewmodel类中包含哪些模块和字段,这个需要开发者自己去衡量,我们建议viewmodel不要引入太多的成员变量,成员变量最好只有上面的提到的5种(context、model……),能不引入其他类型的变量就尽量不要引进来,太多的成员变量对于整个代码结构破坏很大,后面维护的人要时刻关心成员变量什么时候被初始化、什么时候被清掉、什么时候被赋值或者改变,一个细节不小心可能就出现潜在的bug。太多不清晰定义的成员变量又没有注释的代码是很难维护的。

另外,我们会把ui控件的属性和事件都通过xml(如bind:text=@{…})绑定。如果一个业务逻辑要弹一个dialog,但是你又不想在viewmodel里面做弹窗的事(viewmodel不希望做ui相关的事)或者说改变actionbar上面的图标的颜色,改变actionbar按钮是否可点击,这些都不是写在xml里面(都是用java代码初始化的),如何对这些控件的属性做绑定呢?我们先来看下代码:

简单地说你可以对任意的observablefield做监听,然后根据数据的变化做相应ui的改变,业务层viewmodel只要根据业务处理数据就行,以数据来驱动ui。

viewmodel与model的协作

从图1中,viewmodel通过传参数到model层获取网络数据(数据库同理),然后把model的部分数据映射到viewmodel的一些字段(observablefield),并在viewmodel保留这个model的引用,我们来看下这一块的大致代码(代码涉及简单的rxjava,如看不懂可以查阅入门一下):

注1:我们推荐mvvm和rxjava一块儿使用,虽然两者皆有观察者模式的概念,但是rxjava不使用在针对view的监听,更多是业务数据流的转换和处理。databinding框架其实是专用于view-viewmodel的动态绑定的,它使得我们的viewmodel只需要关注数据,而rxjava提供的强大数据流转换函数刚好可以用来处理viewmodel中的种种数据,得到很好的用武之地,同时加上lambda表达式结合的链式编程,使viewmodel的代码非常简洁同时易读易懂。

注2:因为本文样例model层只涉及到网络数据的获取,并没有数据库、存储、数据状态变化等其他业务,所以本文涉及的源码并没有单独把model层抽出来,我们是建议把model层单独抽出来放一个类中,然后以面向接口编程方式提供外界获取和存储数据的接口。

viewmodel与viewmodel的协作

在图1中我们看到两个viewmodel之间用一条虚线连接着,中间写着messenger。messenger可以理解是一个全局消息通道,引入messenger最主要的目的是实现viewmodel和viewmodel的通信,虽然也可以用于view和viewmodel的通信,但并不推荐。viewmodel主要是用来处理业务和数据的,每个viewmodel都有相应的业务职责,但是在业务复杂的情况下,可能存在交叉业务,这时候就需要viewmodel和viewmodel交换数据和通信,这时候一个全局的消息通道就很重要的。

关于messenger的详细使用方法可以参照 mvvm light toolkit 使用指南的 messenger

部分。这里给出一个简单的例子仅供参考:场景是这样的,你的mainactivity对应一个mainviewmodel,mainactivity

里面除了自己的内容还包含一个fragment,这个fragment

的业务处理对应于一个fragmentviewmodel,fragmentviewmodel请求服务器并获取数据。刚好这个数据mainviewmodel也需要用到,我们不可能在mainviewmodel重新请求数据,这样不太合理,这时候就需要把数据传给mainviewmodel,那应该怎么传呢,如果彼此没有引用或者回调?那么只能通过全局的消息通道messenger。

fragmentviewmodel获取消息后通知mainviewmodel并把数据传给它:

mainviewmodel接收消息并处理:

在mainactivity ondestroy取消注册就行了(不然导致内存泄露):

上面的例子只是简单地说明,messenger可以用在很多场景,通知、广播都可以,不一定要传数据,在一定条件下也可以用在view层和viewmodel上的通信和广播,运用范围特别广,需要开发者结合实际的业务中去做更深层次的挖掘。

本文主要讲解了一些个人开发过程中总结的android

mvvm构建思想,更多是理论上各个模块如何分工、代码如何设计。虽然现在业界使用android

mvvm模式开发还比较少,但是随着databinding 1.0的发布,相信在android mvvm

这一领域会更多的人来尝试。刚好我最近用mvvm开发了一段时间,有点心得,写出来仅供参考。

本文和源码都没有涉及到单元测试,如果需要写单元测试,可以结合google开源的mvp框架添加contract类实现面向接口编程,可以帮助你更好地编写单测。同时mvp和mvvm并没孰好孰坏,适合业务、适合自己的才是最有价值的,建议结合google开源的mvp框架和本文介绍的mvvm相关的知识去探索适合自己业务发展的框架。

mvvm light toolkit只是一个工具库,主要目的是更快捷方便地构建android

mvvm应用程序,在里面添加了一些控件额外属性和做了一些事件的封装,同时引进了全局消息通道messenger,个人觉得用起来会比较方便,你也可以尝试一下。当然这个库还有不少地方需要完善和优化,后续也会持续做更新和优化,如果不能达到你的业务需求时,可以clone下来自己做一些相关的扩展。如果想更深入了解mvvm

light toolkit,请看我这篇博文 《mvvm light toolkit 使用指南》。

项目的源码地址 https://github.com/kelin-hong/mvvmlight 。其中:

library是mvvm light toolkit的源码,源码很简单,感兴趣的同学可以看看,没什么技术难度,可以根据自己的需求,添加更多的控件属性和事件绑定。

sample是一个实现知乎日报首页样式的demo,本文的代码示例均出自这个demo。代码包含了一大部分mvvm light

toolkit的使用场景(data、command、messenger均有涉及),同时sample严格按照本博文阐述的mvvm设计思想开发,对理解本文会有比较大的帮助。

本文和源码涉及rxjava+retrofit+lambda如有不懂或没接触过,花点时间入门一下,用到的都是比较简单的东西。

来源:51cto