高效开发 MVVM 和 databinding 你需要使用的工具

5,493 阅读6分钟

喜欢小之的文章的可以关注公众号「WeaponZhi」持续关注动态


相信不少同学已经开始使用MVVM作为自己 Android 开发架构了,但实际上,我在使用过程中查阅资料发现,网上有关 MVVM 的资料并不是很多,这主要是因为 MVVM 还是有一定使用门槛的,并且 MVVM 不一定会帮助你提高开发效率,可能你需要写的代码更多了,或者说为了你为了让代码保持 Databinding 的双向绑定特性,而需要考虑很多业务以外的设计逻辑。我们使用一个架构或者设计模式,当然是为了更好的开发体验嘛,所以我将给大家介绍几个实用的第三方库和工具,来帮助大家解决这些问题。

MVVMLight

「MVVMLight」这个第三方库实际上是对 Databinding 工具库的一些扩展,并且通过ReplyCommandResponseCommand来对所有的 View 的事件进行统一封装,这是我认为 MVVMLight 最大的用处

MVVMLight的官方介绍博客
MVVMLight的源码地址

我们来看一下ReplyCommand怎么用。我们用常见的下拉刷新控件PullToRefreshLayout来举例子。

我们知道如果你想自定义一个控件的事件,你需要使用@BindingAdapter注解,比如ImageView通过URL属性直接根据地址下载图片并显示可以这样写:

@BindingAdapter("bind:urlImage")  
public static void getInternetImage(ImageView iv, String userface) {  
    Picasso.with(iv.getContext()).load(userface).into(iv);  
}
<ImageView  
    android:id="@+id/iv"  
    android:layout_width="wrap_content"  
    android:layout_height="wrap_content"  
    app:urlImage="@{user.urlImage}"/>

这种情况往往是比较简单的,因为只是操作一个属性,但我们要自定义某一个事件该怎么办呢,比如我们要自定义onClick事件,那可能就得写接口了:

@BindingAdapter("setImageOnClick")
    public static void setImageOnClick(ImageView imageView, final ImageOnClickListener listener){
        if (listener != null) {
            imageView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    listener.onClick(v);
                }
            });
        }
    }

interface ImageOnClickListener{
    void onClick(View v);
}

使用的时候呢,你得在 VM 中定义一个ImageOnClickListener的成员变量listener,在里面写具体的onClick实现方法,然后在 xml 中通过app:setImageOnClick="viewModel.listener"来绑定这个事件。

当然,你可以直接通过android:onClick来进行绑定,这里只是实例。

看起来好像也不是很麻烦,但是你可能每一个这样的事件,就得定义一个特殊的接口,我们能不能封装一下呢?

这就是 MVVMLight 中 ReplyCommand 和 ResponseCommand 做的事了。通过这两个类封装了各种请求参数数量和返回值参数数量的回调方法,在使用的时候,只要在泛型里具体指名请求参数和返回值的类型即可,可以说很方便了。

实例,PullToRefreshLayout 是一个刷新列表控件,我们通过使用ReplyCommand监听下拉刷新和上拉加载的监听器是这样写的:

@BindView(R.id.refresh_listview)
PullToRefreshLayout pullToRefreshLayout;

...

@BindingAdapter (value = {"onRefreshCommand", "onLoadCommand"}, requireAll = false)
public static void onRefreshLoadCommand(
    final PullToRefreshLayout pullToRefreshLayout, final ReplyCommand onRefreshCommand, final ReplyCommand onLoadCommand) {

    pullToRefreshLayout.setOnRefreshListener(new PullToRefreshLayout.OnRefreshListener() {
        @Override
        public void onRefresh(PullToRefreshLayout pullToRefreshLayout) {
            if (onRefreshCommand != null) {
                onRefreshCommand.execute();
            }
        }

        @Override
        public void onLoadMore(PullToRefreshLayout pullToRefreshLayout) {
            if (onLoadCommand != null) {
                onLoadCommand.execute();
            }
        }
    });
}

我们使用统一的ReplyCommand来处理控件的各种事件,这里使用的是无参无返回值的最简单的情况,我们在 ViewModel 和 xml 中的写法是和之前的接口差不多的:

public final ReplyCommand onRefreshCommand = new ReplyCommand(() -> getPostData(true));

public final ReplyCommand onLoadCommand = new ReplyCommand(()->getPostData(true));
<com.weapon.joker.lib.view.pullrefreshload.PullToRefreshLayout
    android:id="@+id/pull_refresh_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:onRefreshCommand="@{viewModel.onRefreshCommand}"
    app:onLoadCommand="@{viewModel.onLoadCommand}"/>

这样,我们所有事件的接口就统一了。ResponseCommand 和 ReplyCommand 的区别主要在,ResponseCommand 是用来定义那种有返回值的参数的,而 ReplyCommand 是没有返回值的,具体的使用方法,大家可以参考上面的链接,作者自己讲的最详细。

binding-collection-adapter

「binding-collection-adapter」对所有需要adapter的控件进行了封装,比如一些常用的:ListViewRecyclerViewViewPager等,通过使用这个库,我们就不需要再写 adapter 了,通过 databinding 的方式,在 xml 绑定一些属性,并在 ViewModel 中对这些属性进行处理即可完成这些控件的处理,逻辑清晰,代码简单。

GitHub 地址

下面举一个 RecyclerView 的例子。我们现在 xml 中定义一个 RecyclerView 控件。

<android.support.v7.widget.RecyclerView
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  app:layoutManager="@{LayoutManagers.linear()}"
  app:items="@{viewModel.items}"
  app:itemBinding="@{viewModel.itemBinding}"/>

我们看到有三个特殊的属性:layoutManageritemsitemBinding,这里的layoutManager大家都比较熟悉了,参数是在开头的import导入的,传入相关的类名即可。

<data>
    <variable
        name="viewModel"
        type="com.weaponzhi.test.ViewModel"/>
    <import type="com.weaponzhi.test.LayoutManagers"/>
</data>

我们先来看一下itemBinding是干什么用的,我们知道有时候列表项是可能多布局的,那么这个itemBinding就是用来处理每种布局和对应 item 的 ViewModel 的绑定关系的。上述代码的 ViewModel 中,定义了该itemBinding

public final OnItemBindClass<Object> itemBinding =
    new OnItemBindClass<>
    .map(NoDataViewModel.class,BR.noData,R.layout.listitem_no_data)
    .map(ItemViewModel.class,BR.itemVM,R.layout.listitem_page);

map方法中有三个参数,第一个参数是这个布局的 ViewModel,第三个参数是这个布局的 xml 文件,第二个参数这个 xml 中引入的 ViewModel 的 BR 文件 id。这样我们就绑定好了这个列表控件的多布局逻辑了。一个空数据时候的布局,一个正常返回数据时候的布局。

那么我们的数据是如何刷新的呢,这就要用到上面的items这个属性了,在我们这个例子里,它是这样定义的:

public final ObservableList<Object> viewModels = new ObservableArrayList<>();

当我们网络请求返回的时候,我们在数据回调里,通过对数据类型的处理,进行ItemViewModel的构造,最后只需要将构造好的对象一个个添加到这个ObservableList数据结构中去,界面的刷新工作都在对应的ItemViewModel里中进行处理,我们刚刚设置的itemBinding在这时候就起作用了,当新增数据的时候,它会先判断这个更新数据的ItemViewModel的数据类型,NoDataViewModel.class类型的,那么就使用R.layout.listitem_no_dataItemViewModel.class类型的,就使用R.layout.listitem_page。当然,其他的数据更新和删除操作,也会因为双向绑定而同步刷新。

我们完全从 Adapter 的繁琐中解放出来了!

Databinding support

这是一个 Android Studio 插件,我们写 xml 中的一些 Databind 代码比如<layout><data><variable><import>等标签的使用还是比较多的,而且写起来也比较繁琐,这个插件就是可以帮助你解放双手,只需要在适当的地方按⌥+⏎(Windows 是 Alt+Enter)即可,从官网盗几张 Gif 图给大家感受一下吧。

Wrap with <layout></layout>

Add <data> tag

Wrap with @{}

Wrap with @={}

Switch @{} and @={}

Add <import>

Add <variable>

MVVM 自动代码生成

MVVM 和 MVP 这种架构并不一定会让我们代码量减少,每一个界面可能都要以一种固定的模式创建很多类,那我们为什么不通过一种自动代码生成工具来通过简单的配置就完成这些类的创建呢,Java 完全就可以实现这些功能。网上有很多用 Java 实现的自动生成代码的方式,但每个人实现的 MVP 和 MVVM 架构方式都不同,所以自动化代码也会不同,我来展示下我这边使用的过程吧。

我使用的 MVVM 代码生成工具的主要思路是比较简单粗暴的,通过一个 xml 文件配置一些属性,比如起一个名字,设置一下文件输出的路径,然后在 Java 里用字符串拼接和文件流读取的方式来生成模板代码。


我现在维护的一个项目中,使用了 MVVMLight 和 binding-collection-adapter 大家可以参考下。
GitHub 链接


期待您关注我的公众号:WeaponZhi