十年老 Android:构建 Android-MVVM 应用程序只需这几步?
1. 如何分工
构建 MVVM 框架首先要具体了解各个模块的分工,接下来我们来讲解 View,ViewModel,Model 的它们各自的职责所在。
ViewView 层做的就是和 UI 相关的工作,我们只在 XML 和 Activity 或 Fragment 写 View 层的代码,View 层不做和业务相关的事,也就是我们的 Activity 不写和业务逻辑相关代码,也不写需要根据业务逻辑来更新 UI 的代码,因为更新 UI 通过 Binding 实现,更新 UI 在 ViewModel 里面做(更新绑定的数据源即可),Activity 要做的事就是初始化一些控件(如控件的颜色,添加 RecyclerView 的分割线),Activity 可以更新 UI,但是更新的 UI 必须和业务逻辑和数据是没有关系的,只是单纯的根据点击或者滑动等事件更新 UI(如 根据滑动颜色渐变、根据点击隐藏等单纯 UI 逻辑),Activity(View 层)是可以处理 UI 事件,但是处理的只是处理 UI 自己的事情,View 层只处理 View 层的事。简单的说:View 层不做任何业务逻辑、不涉及操作数据、不处理数据、UI 和数据严格的分开。
ViewModelViewModel 层做的事情刚好和 View 层相反,ViewModel 只做和业务逻辑和业务数据相关的事,不做任何和 UI、控件相关的事,ViewModel 层不会持有任何控件的引用,更不会在 ViewModel 中通过 UI 控件的引用去做更新 UI 的事情。ViewModel 就是专注于业务的逻辑处理,操作的也都是对数据进行操作,这些个数据源绑定在相应的控件上会自动去更改 UI,开发者不需要关心更新 UI 的事情。DataBinding 框架已经支持双向绑定,这使得我们在可以通过双向绑定获取 View 层反馈给 ViewModel 层的数据,并进行操作。关于对 UI 控件事件的处理,我们也希望能把这些事件处理绑定到控件上,并把这些事件统一化,方便 ViewModel 对事件的处理和代码的美观。为此我们通过 BindingAdapter 对一些常用的事件做了封装,把一个个事件封装成一个个 Command,对于每个事件我们用一个 ReplyCommand<T>去处理就行了,ReplyCommand<T>会把可能你需要的数据带给你,这使得我们处理事件的时候也只关心处理数据就行了,再强调一遍 ViewModel 不做和 UI 相关的事。
ModelModel 的职责很简单,基本就是实体模型(Bean)同时包括 Retrofit 的 Service ,ViewModel 可以根据 Model 获取一个 Bean 的 Observable<Bean>( RxJava ),然后做一些数据转换操作和映射到 ViewModel 中的一些字段,最后把这些字段绑定到 View 层上。
2. 如何协作
关于协作,我们先来看下面的一张图:
上图反应了 MVVM 框架中各个模块的联系和数据流的走向,由上图可知 View 和 Model 直接是解耦的,是没有直接联系的,也就是我之前说到的 View 不做任何和业务逻辑和数据处理相关的事。我们从每个模块一一拆分来看。那么我们重点就是下面的三个协作。
ViewModel 与 View 的协作
ViewModel 与 Model 的协作
ViewModel 与 ViewModel 的协作
ViewModel 与 View 的协作
图 2 中 ViewModel 和 View 是通过绑定的方式连接在一起的,绑定的一种是数据绑定,一种是命令绑定。数据的绑定 DataBinding 已经提供好了,简单的定义一些 ObservableField 就能把数据和控件绑定在一起了(如 TextView 的 text 属性),但是 DataBinding 框架提供的不够全面,比如说如何让一个 URL 绑定到一个 ImageView 让这个 ImageView 能自动去加载 url 指定的图片,如何把数据源和布局模板绑定到一个 ListView,让 ListView 可以不需要去写 Adapter 和 ViewHolder 相关的东西,而只是通过简单的绑定的方式把 ViewModel 的数据源绑定到 Xml 的控件里面就能快速的展示列表呢?这些就需要我们做一些工作和简单的封装。关于事件绑定也是一样,MVVM Light Toolkit 做了简单的封装,对于每个事件我们用一个 ReplyCommand<T>去处理就行了,ReplyCommand<T>会把可能你需要的数据带给你,这使得我们处理事件的时候也只关心处理数据就行了。
由 图 1 中 ViewModel 的模块中我们可以看出 ViewModel 类下面一般包含下面 5 个部分:
Context (上下文)
Model (数据模型 Bean)
Data Field (数据绑定)
Command (命令绑定)
Child ViewModel (子 ViewModel)
我们先来看下示例代码,然后在一一讲解 5 个部分是干嘛用的:
//contextprivate Activity context;
//model(数据模型 Bean)private NewsService.News news;private TopNewsService.News topNews;
//数据绑定(data field)public final ObservableField<String> imageUrl = new ObservableField<>();public final ObservableField<String> html = new ObservableField<>();public final ObservableField<String> title = new ObservableField<>();// 一个变量包含了所有关于 View Style 相关的字段 public final ViewStyle viewStyle = new ViewStyle();
//命令绑定(command)public final ReplyCommand onRefreshCommand = new ReplyCommand<>(() -> {
})public final ReplyCommand<Integer> onLoadMoreCommand = new ReplyCommand<>((p) -> {
});
//Child ViewModelpublic final ObservableList<NewItemViewModel> itemViewModel = new ObservableArrayList<>();
/** * ViewStyle 关于控件的一些属性和业务数据无关的 Style 可以做一个包裹,这样代码比较美观,ViewModel 页面也不会有太多的字段。 **/public static class ViewStyle {
public final ObservableBoolean isRefreshing = new ObservableBoolean(true);
public final ObservableBoolean progressRefreshing = new ObservableBoolean(true);}
ContextContext 是干嘛用的呢,为什么每个 ViewModel 都最好需要持了一个 Context 的引用呢?ViewModel 不做和 UI 相关的事,不操作控件,也不更新 UI,那为什么要有 Context 呢?原因主要有以下两点,当然也有其他用处,调用工具类、帮助类可能需要 context 参数等:
通过图 1 中,我们发现 ViewModel 通过传参给 Model 然后得到一个 Observable<Bean>,其实这就是网络请求部分,做网络请求我们必须把 Retrofit Service 返回的 Observable<Bean>绑定到 Context 的生命周期上,防止在请求回来时 Activity 已经销毁等异常,其实这个 Context 的目的就是把网络请求绑定到当前页面的生命周期中。
在图 1 中,我们可以看到两个 ViewModel 之间的联系是通过 Messenger 来做,这个 Messenger 是需要用到 Context,这个我们后续会讲解。
ModelModel 是什么呢,其实就是数据原型,也就是我们用 Json 转过来的 Java Bean,我们可能都知道,ViewModel 要把数据映射到 View 中可能需要大量对 Model 的数据拷贝,拿 Model 的字段去生成对应的 ObservableField(我们不会直接拿 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 的引用,可能会去做一些和 UI 相关的事情,这和我们之前说的 ViewModel 不持有控件的引用,ViewModel 不更改 UI 有相悖。更重要一点是实现一个 Listener 可能需要写一些 UI 逻辑才能最终获取我们想要的,简单一点的比如说,你想要监听 ListView 滑到最底部然后触发加载更多的事件,这时候你就要在 ViewModel 里面写一个 OnScrollListener,然后在里面的 onScroll 方法中做计算,计算什么时候 ListView 滑动底部了,其实 ViewModel 的工作并不想去处理这些事件,它专注做的应该是业务逻辑和数据处理,如果有一个东西它不需要你自己去计算是否滑到底部,而是在滑动底部自动触发一个 Command,同时把当前列表的总共的 item 数量返回给你,方便你通过 page=itemCount/LIMIT+1 去计算出应该请求服务器哪一页的数据那该多好啊。MVVM Light Toolkit 帮你实现了这一点:
public final ReplyCommand<Integer> onLoadMoreCommand = new ReplyCommand<>((itemCount) -> {int page=itemCount/LIMIT+1;loadData(page.LIMIT)});
接着在 XML 布局文件中通过 bind:onLoadMoreCommand 绑定上去就行了
<android.support.v7.widget.RecyclerViewandroid:layout_width="match_parent"
android:layout_height="match_parent"
bind:onLoadMoreCommand="@{viewModel.loadMoreCommand}"/>
当然 Command 并不是必须的,你完全可以依照你的习惯和喜好在 ViewModel 写 Listener,不过使用 Command 可以使你的 ViewModel 更简洁易读,你也可以自己定义更多的 Command,自己定义其他功能 Command,那么 ViewModel 的事件处理都是托管 ReplyCommand<T>来处理,这样的代码看起来会特别美观和清晰。
Child ViewModel (子 ViewModel)子 ViewModel 的概念就是在 ViewModel 里面嵌套其他的 ViewModel,这种场景还是很常见的。比如说你一个 Activity 里面有两个 Fragment,ViewModel 是以业务划分的,两个 Fragment 做的业务不一样,自然是由两个 ViewModel 来处理,Activity 本身可能就有个 ViewModel 来做它自己的业务,这时候 Activity 的这个 ViewModel 里面可能包含了两个 Fragment 分别的 ViewModel。这就是嵌套的子 ViewModel。还有另外一种就是对于 AdapterView 如 ListView RecyclerView,ViewPager 等。
//Child ViewModelpublic finalObservableList<ItemViewModel> itemViewModel = new ObservableArrayList<>();
它们的每个 Item 其实就对应于一个 ViewModel,然后在当前的 ViewModel 通过 ObservableList<ItemViewModel>持有引用(如上述代码),这也是很常见的嵌套的子 ViewModel。我们其实还建议,如果一个页面业务非常复杂,不要把所有逻辑都写在一个 ViewModel,可以把页面做业务划分,把不同的业务放到不同的 ViewModel,然后整合到一个总的 ViewModel,这样做起来可以使我们的代码业务清晰,简短意赅,也方便后人的维护。
总得来说 ViewModel 和 View 之前仅仅只有绑定的关系,View 层需要的属性和事件处理都是在 xml 里面绑定好了,ViewModel 层不会去操作 UI,只会操作数据,ViewModel 只是根据业务要求处理数据,这些数据自动映射到 View 层控件的属性上。关于 ViewModel 类中包含哪些模块和字段,这个需要开发者自己去衡量,这边建议 ViewModel 不要引入太多的成员变量,成员变量最好只有上面的提到的 5 种(context、model、...),能不进入其他类型的变量就尽量不要引进来,太多的成员变量对于整个代码结构破坏很大,后面维护的人要时刻关心成员变量什么时候被初始化,什么时候被清掉,什么时候被赋值或者改变,一个细节不小心可能就出现潜在的 Bug。太多不清晰定义的成员变量又没有注释的代码是很难维护的。
2016 8 月 25 日更新:我们会把 UI 控件的属性和事件都通过 xml 里面(如 bind:text=@{...})绑定,但是如果一个业务逻辑要弹一个 Dialog,但是你又不想在 ViewModel 里面做弹窗的事(ViewModel 不做 UI 相关的事)或者说改变 ActionBar 上面的图标的颜色,改变 ActionBar 按钮是否可点击,这些都不是写在 xml 里面(都是用 java 初始化话),如何对这些控件的属性做绑定呢?我们先来看下代码:
public class MainViewModel implements ViewModel {....//true 的时候弹出 Dialog,false 的时候关掉 dialogpublic final ObservableBoolean isShowDialog = new ObservableBoolean();.........}// 在 View 层做一个对 isShowDialog 改变的监听 public class MainActivity extends RxBasePmsActivity {
private MainViewModel mainViewModel;
@Overrideprotected void onCreate(Bundle savedInstanceState) {.....mainViewModel.isShowDialog.addOnPropertyChangedCallback(new android.databinding.Observable.OnPropertyChangedCallback() {@Overridepublic void onPropertyChanged(android.databinding.Observable sender, int propertyId) {if (mainViewModel.isShowDialog.get()) {dialog.show();} else {dialog.dismiss();}}});}...}
简单的说你可以对任意的 ObservableField 做监听,然后根据数据的变化做相应 UI 的改变,业务层 ViewModel 只要根据业务处理数据就行,以数据来驱动 UI。
ViewModel 与 Model 的协作 从图 1 中,Model 是通过 Retrofit 去获取网络数据的,返回的数据是一个 Observable<Bean>( RxJava ),Model 层其实做的就是这些。那么 ViewModel 做的就是通过传参数到 Model 层获取到网络数据(数据库同理)然后把 Model 的部分数据映射到 ViewModel 的一些字段(ObservableField),并在 ViewModel 保留这个 Model 的引用,我们来看下这一块的大致代码(代码涉及到简单 RxJava,如看不懂可以查阅入门一下):
//Modelprivate NewsDetail newsDetail;
private void loadData(long id) {
// Observable<Bean> 用来获取网络数据 Observable<Notification<NewsDetailService.NewsDetail>> newsDetailOb =
RetrofitProvider.getInstance().create(NewsDetailService.class)
.getNewsDetail(id)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())// 将网络请求绑定到 Activity 的生命周期.compose(((ActivityLifecycleProvider) context).bindToLifecycle())//变成 Notification<Bean> 使我们更方便处理数据和错误.materialize().share();
// 处理返回的数据 newsDetailOb.filter(Notification::isOnNext)
.map(n -> n.getValue())
// 给成员变量 newsDetail 赋值,之前提到的 5 种变量类型中的一种(model 类型)
.doOnNext(m -> newsDetail = m)
.subscribe(m -> initViewModelField(m));
// 网络请求错误处理 NewsListHelper.dealWithResponseError(newsDetailOb.filter(Notification::isOnError)
.map(n -> n.getThrowable()));}//Model -->ViewModelprivate void initViewModelField(NewsDetail newsDetail) {
viewStyle.isRefreshing.set(false);
imageUrl.set(newsDetail.getImage());
Observable.just(newsDetail.getBody()).map(s -> s + "<style type="text/css">" + newsDetail.getCssStr())
.map(s -> s + "</style>")
.subscribe(s -> html.set(s));
title.set(newsDetail.getTitle());}
以上代码基本把注释补全了,基本思路比较清晰,,Rxjava 涉及的操作符都是比较基本的,如有不懂,可以稍微去入门,之后的源码里面 ViewModel 数据逻辑处理都是用 Rxjava 做,所以需要提前学习一下方便你看懂源码。
注:我们推荐使用 MVVM 和 RxJava 一块使用,虽然两者皆有观察者模式的概念,但是我们 RxJava 不使用在针对 View 的监听,更多是业务数据流的转换和处理。DataBinding 框架其实是专用于 View-ViewModel 的动态绑定的,它使得我们的 ViewModel 只需要关注数据,而 RxJava 提供的强大数据流转换函数刚好可以用来处理 ViewModel 中的种种数据,得到很好的用武之地,同时加上 Lambda 表达式结合的链式编程,使 ViewModel 的代码非常简洁同时易读易懂。
ViewModel 与 ViewModel 的协作 在图 1 中 我们看到两个 ViewModel 之间用一条虚线连接着,中间写着 Messenger,Messenger 可以理解是一个全局消息通道,引入 messenger 最主要的目的就实现 ViewModel 和 ViewModel 的通信,也可以用做 View 和 ViewModel 的通信,但是并不推荐这样做。ViewModel 主要是用来处理业务和数据的,每个 ViewModel 都有相应的业务职责,但是在业务复杂的情况下,可能存在交叉业务,这时候就需要 ViewModel 和 ViewModel 交换数据和通信,这时候一个全局的消息通道就很重要的。这边给出一个简单的例子仅供参考:场景是这样的,你的 MainActivity 对应一个 MainViewModel,MainActivity 里面除了自己的内容还包含一个 Fragment,这个 Fragment 的业务处理对应于一个 FragmentViewModel,FragmentViewModel 请求服务器并获取数据,刚好这个数据 MainViewModel 也需要用到,我们不可能在 MainViewModel 重新请求数据,这样不太合理,这时候就需要把数据传给 MainViewModel,那么应该怎么传,彼此没有引用或者回调。那么只能通过全局的消息通道 Messenger。
FragmentViewModel 获取消息后通知 MainViewModel 并把数据传给它:
combineRequestOb.filter(Notification::isOnNext)
.map(n -> n.getValue())
.map(p -> p.first)
.filter(m -> !m.getTop_stories().isEmpty())
.doOnNext(m ->Observable.just(NewsListHelper.isTomorrow(date)).filter(b -> b).subscribe(b -> itemViewModel.clear()))// 上面的代码可以不看,就是获取网络数据 ,通过 send 把数据传过去.subscribe(m -> Messenger.getDefault().send(m, TOKEN_TOP_NEWS_FINISH));
MainViewModel 接收消息并处理:
Messenger.getDefault().register(activity, NewsViewModel.TOKEN_TOP_NEWS_FINISH, TopNewsService.News.class, (news) -> {// to something....}
在 MainActivity onDestroy 取消注册就行了(不然导致内存泄露)
@Overrideprotected void onDestroy() {
super.onDestroy();
Messenger.getDefault().unregister(this);}
当然上面的例子也只是简单的说明下,Messenger 可以用在很多场景,通知,广播都可以,不一定要传数据,在一定条件下也可以用在 View 层和 ViewModel 上的通信和广播。运用范围特别广,需要开发者结合实际的业务中去做更深层次的挖掘。
4、总结和源码 ###
本篇博文讲解主要是一些个人开发过程中总结的 Android MVVM 构建思想,更多是理论上各个模块如何分工,代码如何设计,虽然现在业界使用 Android MVVM 模式开发还比较少,但是随着 DataBinding 1.0 的发布,相信在 Andr
oid MVVM 这块领域会更多的人来尝试,刚好最近用 MVVM 开发了一段时间,有点心得,写出来仅供参考。
文中讲解的过程代码比较少,代码用到了自己开发的一个 MVVM Light Toolkit 库,而且还是 RxJava + Lambda 的代码,估计很多人看着都晕菜了,这边会把源码公布出来。如果你还没有尝试过用 RxJava+Retrofit+DataBinding 构建 Android MVVM 应用程序,那么你可以试着看一下这边的源码并且做一下尝试,说不定你会喜欢上这样的开发框架。
评论