概述
MVVM Light Toolkit是一个Android MVVM 轻量级工具库,主要目的是更快捷方便的构建Android MVVM应用程序,工具库添加了一些Data Binding 不支持的属性,还有添加对控件事件的封装,同时提个一个全局消息通道方便ViewModel 之间的通信,Toolkit主要包括两部分Binding和Messenger,接下来,我们分别说明下这两个模块的作用和使用方法。
源码:
Github:https://github.com/Kelin-Hong/MVVMLight
compile 'com.kelin.mvvmlight:library:1.0.0'
Binding
由上图我们可以看到,在View和ViewModel的绑定中,包含两种绑定,一种是数据的绑定(比如:TextView:text),另外一种命令绑定,命令绑定我们可以理解为事件绑定,(比如:Button:click),但是目前Databinding 并不完全支持命令的绑定, 而且对Data的绑定的支持也不完善(比如说不支持AdapterView对DataSource的绑定),那么在MVVM LightBinding我们添加了部分Data绑定支持和Command绑定的支持。
-
Data 绑定
我们添加了(ImageView:uri、placeholderImageRes),(ListView:views、itemView) 等部分控件的一些属性,这使得我们用起来就方便很多,例如我们只要在xml布局<ImageView>标签中设置uri,那么这个图片就能自动去加载这个uri的图片,如下:<ImageView android:layout_width="match_parent" android:layout_height="match_parent" android:layout_alignParentRight="true" bind:uri="@{viewModel.imageUrl}" bind:placeholderImageRes="@{R.drawable.ic_launcher}"/>
当然ImageView还支持其他属性
placeholderImageRes :图片还没有下载完成时的替换图片
request_width : 请求图片的宽度,会自动帮你裁剪
request_height:请求图片的高度,会自动帮你裁剪
接下来看下示例:
对于ListView、RecyclerView 和 ViewPager 等AdapterView使用起来就更加简单了,在布局文件添加bind:itemView(子布局模板),bind:items(数据源)还有bind:layoutManager就能ok了,完全不需要写Adapter相关的东西。
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
bind:itemView="@{viewModel.itemView}"
bind:items="@{viewModel.itemViewModel}"
bind:layoutManager="@{LayoutManagers.linear()}"
然后Java 代码中在ViewModel定义itemView和itemViewModel就可以了列表就能展示出来了
public final ObservableList<ViewModel> itemViewModel = new ObservableArrayList<>();
public final ItemView itemView = ItemView.of(BR.viewModel, R.layout.layoutitem_list_view);
动画示例:
关于AdapterView 数据源绑定问题主要还是Google的DataBinding框架还不够完善,相信不久后肯定有更好的方案出来,让我们更加方便把数据源和布局模板绑定到ListView上,当然现在也有一些开源库帮我们把这个部分做好了,我们也不重复造轮子,就直接使用了关于AdapterView的一些数据绑定扩展,https://github.com/evant/binding-collection-adapter 给了比较详细的描述,基本能满足大部分AdapterView的需求。
接下来列举目前MVVM Light 工具库支持的一些Data Binding的属性:
-
ImageView
<attr name="uri" /> <!--width for ResizeOptions (use Fresco to load bitmap). --> <attr name="request_width" format="integer" /> <!--height for ResizeOptions (use Fresco to load bitmap). --> <attr name="request_height" format="integer" /> <attr name="placeholderImageRes" format="reference|color" />
-
ListView、ViewPager、RecyclerView
<!-- require ItemView or ItemViewSelector --> <attr name="itemView" /> <!-- require List<ViewModel> bind to ItemView to presentation.--> <attr name="items" /> <!-- require a adapter which type of BindingRecyclerViewAdapter<T> to AdapterView--> <attr name="adapter" /> <attr name="dropDownItemView" format="reference" /> <attr name="itemIds" format="reference" /> <attr name="itemIsEnabled" format="reference" /> <!-- require PageTitles<T>--> <attr name="pageTitles" format="reference" />
-
ViewGroup
<!-- require ItemView or ItemViewSelector --> <attr name="itemView" /> <!-- require List<ViewModel> bind to ItemView to presentation.--> <attr name="viewModels" format="reference" />
-
EditText
<!-- require boolean value to decide whether requestFocus for view. --> <attr name="requestFocus" format="boolean" />
-
SimpleDraweeView
<!-- require String to load Image"--> <attr name="uri" />
-
WebView
<!-- require String render to html show in webview--> <attr name="render" format="string" />
目前整理的一些常用属性和控件可能不足,我们也不太可能能把所有可用的控件和控件关联的属性全部做一些封装,希望开发者自己可以在需要的时候去继承相应的类扩展更多的属性。
-
Command 绑定
Command 翻译为命令,就是控件发号施令,然后有人去回复处理这个命令,比如Button 发出一个Click Command,那么应该有个处理者来处理这个命令,我们把这个处理者叫做ReplyCommand,我们把ReplyCommand绑定到相应的控件上,如果控件发出Event的时候,就会找到这个ReplyCommand让它来处理这个事件,说得简单一些就是我们对控件一些事件做了简单的封装,使得处理这些事件我们只有关注我们相应事件传递给我们想要的数据,其他UI相关的逻辑并不需要我们去关心。接下来我们简要说明具体使用方法:<android.support.v7.widget.RecyclerView android:id="@+id/recyclerView" android:layout_width="match_parent" android:layout_height="match_parent" bind:onLoadMoreCommand="@{viewModel.loadMoreCommand}"/>
bind:onLoadMoreCommand: RecyclerView 在滑到最底部的时候自动触发这个事件,这个事件的处理者是一个ReplyCommand<Integer>类型的变量
接下来我们在ViewModel 里面的定义一个ReplyCommand<Integer> 变量
public final ReplyCommand<Integer> loadMoreCommand = new ReplyCommand<>( (count) -> { /*count 代表当前RecyclerView 有多少个Item,通过这个值我们可以 得到当前应该去加载第几页的数据*/ int page=count / LIMIT; loadData(page) });
对于处理RecyclerView 下拉加载更多,我们之前的做法可能需要去写各种代码来判断是否滑动底部,在OnScrollListener 里面做一些计算然后才知道滑到底部了,但是如果用封装好的Command事件,你的ViewModel里面的代码将非常简洁,只要简要的声明一个 ReplyCommand<Integer>,它就能自动传一个当前List中Item的总数给你,你要做的只是专注你的业务处理(比如 加载数据),ViewModel 里面只处理数据,不处理控件,没有任何控件的引用,所以UI的状态和变化都由数据来绑定控制的,这样数据就成了主角,我们在ViewModel 里面只要和数据打交道就可以了。
动画示例:
同时对于我最常见的点击事件,我们也做对了click事件的封装,封装成一个clickCommand,接受的参数是一个无参的ReplyCommand:
动画示例:
关于下拉刷新控件SwipeRefreshLayout的下拉刷新事件也简单封装成另一个ReplyCommand
动画示例:
接下来列举目前MVVM Light 工具库支持的一些Command Binding的属性:
-
View
<!-- require ReplyCommand to deal with view click event. --> <attr name="clickCommand" format="reference" /> <!-- require ReplyCommand<Boolean> to deal with view focus change event. ReplyCommand would has params which means if view hasFocus.--> <attr name="onFocusChangeCommand" format="reference" /> <!-- require ReplyCommand<MotionEvent> --> <attr name="onTouchCommand" />
-
ListView、RecyclerView
<!-- require ReplyCommand<Integer> --> <attr name="onScrollStateChangedCommand" /> <!-- require ReplyCommand<ListViewScrollDataWrapper> --> <attr name="onScrollChangeCommand" /> <!-- require ReplyCommand<Integer> count of list items--> <attr name="onLoadMoreCommand" format="reference" />
-
ViewPager
<!--require ReplyCommand<ViewPagerDataWrapper> --> <attr name="onPageScrolledCommand" format="reference" /> <!--require ReplyCommand<Integer> --> <attr name="onPageSelectedCommand" format="reference" /> <!--require ReplyCommand<Integer> --> <attr name="onPageScrollStateChangedCommand" format="reference" />
-
EditText
<!--require ReplyCommand<TextChangeDataWrapper> --> <attr name="beforeTextChangedCommand" format="reference" /> <!--require ReplyCommand<TextChangeDataWrapper> --> <attr name="onTextChangedCommand" format="reference" /> <!--require ReplyCommand<String> --> <attr name="afterTextChangedCommand" format="reference" />
-
ImageView
<!-- require ReplyCommand<Bitmap> --> <attr name="onSuccessCommand" format="reference" /> <!--require ReplyCommand<CloseableReference<CloseableImage>> --> <attr name="onFailureCommand" format="reference" />
-
ScrollView、NestedScrollView
<!-- require ReplyCommand<ScrollDataWrapper> --> <attr name="onScrollChangeCommand" /> <!-- require ReplyCommand<NestScrollDataWrapper> --> <attr name="onScrollChangeCommand" />
-
SwipeRefreshLayout
<!-- require RelayCommand<> --> <attr name="onRefreshCommand" format="reference" />
以上的Command有继承效应,即ImageView、TextView是View的子类,所以它们都拥有View 扩展的Data Binding和Command Binding 如:clickCommand 等。
注:
- MVVMLight 目前只支持上面提及的部分的控件Data和Command绑定,还有大部分的控件一些属性和事件并没有去封装,我们希望您根据自己的需求去补充,包括一些你用的自定义控件和第三方控件,都可以简单封装成一个Command,这样使得我们的ViewModel 更加简洁同时是我们更能专心处理数据和业务,不关心控件和UI。
- 上面涉及的属性和Command,我们在attrs文件都已经声明,您在xml文件使用中将可以自动提示相应的名称,比如你在TextView中打bind:的时候和TextView相关的属性都会提示(如:requestFocus、clickCommand、等相关的属性),但是属性后面需要哪种ReplyCommand<T>,即T应该是什么类型,就需要查一下相应的文档、或者查一下源码。
- 由于没办法做到在xml输入属性(如bind:clickCommand)自动提示应该输入什么值接入什么参数,所以这时候最简单的方式就是去查源码,我们把每个控件都单独放到一个包里面,方便查找源码。如果你没找到源码的位置,你可以shift+command+o 然后输入ViewBindingAdapter(也就是打开文件ViewBindingAdapter.class 或者 ViewBindingAdapter.java )然后选择相应的控件,去查看该控件的支持哪些属性和Command同时可以查看需要的ReplyCommand的类型。
- 虽然是ViewBindingAdapter 里面的方法是static,但是是支持重写的,保持参数和@BindingAdapter(value =*) 的值不变,DataBinding 的找方法的时候就优先采用你写的方法,而不是库里的方法,也就是如果你对ViewBindingAdapter的里面的static方法的实现不满意的是可以重写的。
Messenger
引入messenger最主要的目的就实现ViewModel和ViewModel的通信,也可以用做View和ViewModel的通信,但是并不推荐这样做。ViewModel主要是用来处理业务和数据的,每个ViewModel都有相应的业务职责,但是在业务复杂的情况下,可能存在交叉业务,这时候就需要ViewModel和ViewModel交换数据和通信,这时候一个全局的消息通道就很重要的。接下来我们简单看一下messenger的使用:
- 全局广播消息,没有参数
/* 全局发送一个消息,但是不传任何参数给接收者 TOKEN: 相当于broadcast的Action,谁注册了这个令牌,相当于准备接收这个消息*/ Messenger.Default().sendNoMsg(TOKEN); /* context: 一般是activity,代表接收者,用于在取消注册的时候使用unRegister(context)可快速取消这个context里面的所有注册 TOKEN: 相当于broadcast的Action,谁注册了这个令牌,相当于准备接收这个消息 (data)->{ }: 处理消息的Action */ Messenger.Default().register(context, TOKEN, () -> { });
- 全局广播消息,带有参数
/*发送消息,传递参数data(任何类型都可),TOKEN 是一个令牌(String类型)相当于 broadcast的Action,谁注册了这个令牌,相当于准备接收这个消息*/ Messenger.getDefault().send(data, TOKEN) /* context: 同上 * TOKEN: 相当于broadcast的Action,谁注册了这个令牌,相当于准备接收这个消息 * Data.class: 传递过来参数的类型(如 String.class,Model.class) * (data)->{ }: 处理消息的function,参数是传递过来的 */ Messenger.getDefault().register(context, TOKEN, Data.class, (data) -> { });
示例:
- 发送到指定的接收器(不常用)
/*target 代表接收对象,一般是Activity(ViewModel 持有Activity的引 用),直接将消息发到指定的目标,而不是广播*/ Messenger.getDefault().sendToTarget(T message, R target) Messenger.getDefault().sendNoMsgToTargetWithToken(Object token,R target) Messenger.getDefault().sendNoMsgToTarget(Object target)
-
取消注册
/*一般在Activity的OnDestroy()方法中调用就可以, 因为我们一般注册的时候, 第一个参数 recipient就是传的当前的context,所以只要取消注册,这个Activity就再也收不到任何的消息了*/ Messenger.getDefault().unregister(Object recipient)"
Messenger在全局虽然传递数据和通知非常方便,但是建议不要滥用,消息通知太多也意味着耦合性太高,代码框架设计不够友好,同时影响代码的可读性,ViewModel和View最好是用绑定的方式去处理UI和事件,ViewModel 和ViewModel 一些数据的通信交互才用Messenger来传递会好一些。
注:
1、Messenger 一定要取消注册,不然会造成严重的内存泄露
2、Messenger 的TOKEN在命名的时候要注意,最好和相关的ViewModel结合起来,避免在同一个App出现两个相同的Token,这样会把消息发到其他你不知道的地方,造成潜在的BUG!