MVVM之DataBinding入门

  |     |   本文总阅读量:

MVVM介绍

本篇这是基础入门篇,要想看源码剖析请移步到 DataBinding实现原理探析

MVVM框架类似于早期的MVC和最热的MVP,但是比起这两个更为强势。MV-VM相比于MVP,其实就是将Presenter层替换成了ViewModel层,我们都知道,MVP的好处就是将逻辑代码从View层抽离出来,做到与UI层的低耦合,但是无形中会创造出许多的接口,有些接口很是冗余,不仅如此,当后期修改数据或者添加新的功能还需要修改或是添加接口,很是麻烦。

这个时候MV-VM的优势就体现出来了,ViewModel层所需要做的完全就是跟逻辑相关的代码,完全不会涉及到UI。当数据变化,直接驱动UI的改变,中间省去了冗余的接口。同时,在ViewModel层编写代码中,要求开发者需要将每个方法尽可能的做的功能单一,不与外部有任何的引用或者是联系,无形中提高了代码的健壮性,方便了后期的单元测试。

DataBinding其实就是谷歌出台的工具,是实现UI和数据绑定的框架,ViewViewModel通过DataBinding实现单向绑定或双向绑定,做到UI和数据的相互监听,同时开发者的任务分配也就很明确了,负责ViewModel的小伙伴完全不用考虑UI如何实现,很大程度上提高了代码的开发效率和后期出问题跟踪的准确性,针对这些好处,采用MVVM进行代码开发还是非常有必要的。

初步使用

1. modulebuild.gradle文件加上一行配置代码

android {
    ...
    dataBinding {
        enabled = true
    }
}

2. 创建布局文件

只需要在之前布局的基础上,外层嵌套 <layout></layout>即可。

<layout
    xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <variable
            name="student"
            type="com.xiaweizi.bean.Student"/>
        <!-- 这里 type 必须传完整路径,或者用 import 方式也是可以的 -->
        <!--
            <import type="com.xiaweizi.bean.Student"/>
            <variable
                name="student"
                type="Student"/>
        -->
    </data>

    <!-- 对应之前的XML文件 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center_horizontal"
        android:orientation="vertical">

    </LinearLayout>
</layout>

因为XML是不支持自定义导包的,所以通过import先导包,如果类名相同的话可以通过alias进行区分:

<import type="android.view.View"/>
<import type="com.xiaweizi.View"
        alias="MyView"/>

<variable
    name="view1"
    type="View"/>

<variable
    name="view2"
    type="MyView"/>

这个时候会在app\build\generated\source\debug\包名路径下生成对应的binding类,命名方式,举个例子最为直接:

原XML名:activity_main  ----> 生成对应的binding名: ActivityMainBinding

3. Activity中替换原来的setContentView()代码

ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);

4. 接下来就是关键的ViewModel

a. 单向绑定

咱们先从简单的开始,DataBinding有个很大的好处就是摒弃原生findViewById频繁的遍历视图层和ButterKnife的反射,采用的是数组记录每个view

final Object[] bindings = mapBindings(bindingComponent, root, 8, sIncludes, sViewsWithIds);

XML创建一个TextView

<TextView
    android:id="@+id/tv_content"
    android:text="@{student.name}"
    android:layout_width="match_parent"
    android:layout_height="50dp"/>

在代码中通过binding直接可以获取到这个TextView

mBinding.tvContent

那么如何实现单向绑定呢?

Student student = new Student("xiaweizi", 12);
mBinding.setStudent(student);

这样就可以直接改变TextView的值。

ViewModel就是简单的数据

public class Student {
    public String name;
    public int age;
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

b. 双向绑定

之前说的单向绑定,即当数据变化,通过mBinding.setStudent(student)方式驱动UI的改变
而双向绑定,无论View还是ViewModel谁改变,都会驱动另一方的改变,实现双向绑定有两种方式:继承BaseObservable和使用ObservableField创建成员变量。

代码实现:
第一种继承BaseObservable

public class Student extends BaseObservable{

    // 如果是 public 则在成员变量上方加上 @Bindable 注解
    @Bindable
    public String sex;

    public void setSex(String sex) {
        this.sex = sex;
        notifyPropertyChanged(BR.sex);
    }

    /*************************** 我是分割线 ***************************/
    // 如果是 private 则在成员变量的 get 方法中添加 @Bindable 注解
    private String name;
    @Bindable
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
        notifyPropertyChanged(BR.name);
    }

    public void setSexName(String name, String sex){
        this.name = name;
        this.sex = sex;
        notifyChange();
    }
}

这个时候当调用setName()方法,不仅数据改变,UI中的TextView内容也会随之改变。

我们可以发现有两个方法:notifyPropertyChanged()notifyChange,一个是更新指定的变量,第二个是更新所有该ViewModel中的对象。

notifyPropertyChanged(int fieldId)里面传的参数,即上面通过@Bindable注解创建对应的变量id

第二种:使用ObservableField

public class Student extends BaseObservable{

    public ObservableField<String> name = new ObservableField<>();

    private ObservableInt age = new ObservableInt();
    public void setAge(int age) {
        this.age.set(age);
    }
    public int getAge() {
        return age.get();
    }
}

通过使用ObservableField创建的对象作用相当于第一种的方案,支持ObservableIntObservableBoolean或者是ObservableField<T>指定的类型、ObservableArrayMap<String, Object>ObservableArrayList<Object>等。

ObservableField内部已经封装了getset方法,如果成员变量是public属性,直接通过

mStudent.name.set("shabi");
String name = mStudent.name.get();

设置和获取对应的成员变量的值。

其他使用

学会了上面基本的用户还是远远不够的,像按钮的点击事件或是EditText内容的监听,这些也是非常重要的,不过学会了一种,其他的举一反三就会容易的多了。

1. 事件处理

dataBinding需要你通过一些表达式来处理view的分发事件,除了少数例子外,事件元素的名称是由监听器中的方法所控制。比如View.OnLongClickListener内部有onLongClick()方法,所以XML定义的事件就为android:onLongClick.

可以直接在Activity内部定义一个类,用于处理事件的监听

public class Presenter {
    public void onClickExample(View view) {
        Toast.makeText(SimpleActivity.this, "点到了", Toast.LENGTH_SHORT).show();
    }

    public void onTextChanged(CharSequence s, int start, int before, int count) {
        mStudent.name.set(s.toString());
    }

    public void onClickListenerBinding(Student student) {
        Toast.makeText(SimpleActivity.this, student.name.get(),Toast.LENGTH_SHORT).show();
    }
}

XML中:

<EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:hint="输入name"
    android:onTextChanged="@{presenter::onTextChanged}"/>


<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:onClick="@{presenter.onClickExample}"
    android:text='@{"年龄:" + student.age}'/>

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginLeft="5dp"
    android:onClick="@{() -> presenter.onClickListenerBinding(student)}"
    android:text='@{"姓名:" + student.name}'/>

首先从点击事件开始分析,android:onClick="@{presenter.onClickExample}" 里面对应的方法自然是要与Presenter定义的方法名一致,名字可以不为onClickExample,但是参数必须是View,参数要对应于setOnClickListener(onClickListener listener)对应的onClickListener要实现的接口,即public void onClick(View)

同理,监听EditText文本的变化,一般只要注意onTextChanged(CharSequence s, int start, int before, int count)方法即可,那么我们可以创建与之对应的方法,在XML文件中引用:android:onTextChanged="@{presenter::onTextChanged}"

最后再来看从UI中获取数据,也就是数据的回调,即DataBinding的精髓支出,ViewViewModel双向绑定。android:onClick="@{() -> presenter.onClickListenerBinding(student)}这里用到了lamda表达式,这样就可以不遵循默认的方法签名,将student对象直接传回点击方法中。来看一下实现效果:

简单测试.gif

一目了然,我就不赘述了,我们可以发现一点,一开始我们并没有给Student对象设置值,所以显示的是null,并没有报空指针异常,这也是DataBinding的有点之一。

其实dataBinding自带对数据监听的方法:

<EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@={student.name}"/>

代码中:

student.addOnPropertyChangedCallback(new Observable.OnPropertyChangedCallback() {
        @Override
        public void onPropertyChanged(Observable observable, int i) {
            // i 为 BR 文件中对应的 int 值
            Log.i("xwz--->", student.getName());
            Log.i("xwz--->", student.getAge());
        }
});

这个对数据的监听建立在,使用@Bindable作为双向绑定为条件,当数据变化,便会出发onPropertyChanged方法。需要注意的是android:text="@={student.name}",@后面多了一个=

2. ViewStubinclude

dataBinding同样是支持ViewStub的,使用起来也很简单,直接贴代码了。

<ViewStub
    android:id="@+id/view_stub"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout="@layout/viewstub"/>

代码中:

View inflate = binding.viewStub.getViewStub().inflate();

inflate即为替代ViewStubView.

至于include更简单,用法跟以前是差不多,唯一不同的是可以将ViewModel传到下一个XML中:

<include layout="@layout/layout_include" bind:student="@{student}"/>

layout_include中同样可以共享student这个对象。

3. BindingAdapter的使用

我们之前用的都是Android自带的监听或是属性,比如textonClick,但是如果项目中需要动态改变ImageView的内容,那我们应该怎么办呢?dataBinding给我们提供了BindingAdapter这个注解,方便我们定义自定义的属性。
假如我们有个需求,点击按钮更换图片,这个时候我们需要定义静态的方法:

@BindingAdapter({"url", "name"})
public static void loadImageView(ImageView view, String url, String name) {
    Log.i("xwz--->", url + "\t" + name);
    Glide.with(view.getContext())
         .load(url)
         .into(view);
}

XML中使用

<ImageView
    android:layout_width="160dp"
    android:layout_height="160dp"
    bind:name="@{student.name}"
    bind:url="@{student.imgUrl}"/>

这里有必要解释一下,静态方法loadImageView里第一个参数为作用的View,这里是ImageView;后面的参数即分别对应于@BindingAdapter里面的参数。那这里是怎么跟View联系在一块呢?我们发现XML中有这样一行代码bind:name="@{student.name}这里的name对应的的@BindingAdapter注解里的参数name,并映射于ViewModel中的student.name。当student.name值改变,就会触发loadImageView方法,从而执行里面的方法。

bind名称是任意的定义的,不过要定义对应的命名空间xmlns:bind="http://schemas.android.com/apk/res-auto"

实现的效果就很简单了:

bindAdapter.gif

更强大的在于可以覆盖Android原生的元素设置属性,比如android:text最常见不过了

@BindingAdapter ("android:text")
public static void setText(TextView view, String text) {
    view.setText(text + "xiaweizi");
    Log.i("xwz--->", text);
}

XML:

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text='@{"测试"}'/>

这个时候所有设置text的地方后缀全部加上了xiaweizi.

4. @BindingConversion

dataBinding还支持对数据的转换,或者是类型的转换

@BindingConversion
public static String addString(String text){
    Log.i("xwz--->", "DemoBindingAdapter:  " + "addString: " + text);
    return text + "xiaweizi";
}

这个时候会将项目中所有以@{String}方式用到的String后缀全部加上xiaweizi.

@BindingConversion
public static ColorDrawable convertColorToDrawable(int color){
   return new ColorDrawable(color);
}

XML:

<View
   android:background="@{isError ? @color/red : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

这段代码的作用在于将int类型的color值,转换成了ColorDrawable类型.

5. DataBindingComponent

通过BindingAdapter是可以增加一些自定义的属性或者是修改Android原生的属性,但是它有一个弊端,就是全局修改所有的相关属性,不过配合上DataBindingComponent就可以解决这个问题。

DataBindingUtil.setContentView(this, R.layout.activity_component, new FirstComponent());

开始的是以一个参数的形式传入这个View中,那么只作用于当前的view。

DataBindingUtil.setDefaultComponent(new FirstComponent());

或者是这种设置全局的方式,也可以改变。

DataBindingComponent其实是一个空方法的接口,你需要先创建一个拥有@BindingAdapter的类,这里就不能定义为public,因为这样DataBindingComponent就找不到对应的类,我们为了方便后期的开发,可以定义一个抽象类:

public abstract class AbstractAdapter {
    @BindingAdapter ("text")
    public abstract void setText(TextView textView, String text);
}

然后定义一个实现类:

public class FirstAdapter extends AbstractAdapter{
    @Override
    public void setText(TextView textView, String text) {
        Log.i("xwz--->", "FirstAdapter:  " + "setText: ");
        textView.setText(text+"first");
    }
}

这个时候当你创建一个实现DataBindingComponent接口的类时,会发现让你实现一个方法:

public class FirstComponent implements android.databinding.DataBindingComponent {
    @Override
    public AbstractAdapter getAbstractAdapter() {
        return new FirstAdapter();
    }
}

这里返回的就是创建的adapter,可以根据需求创建对应的component.

RecyclerView中的应用

除了最基本的使用,还有一个频繁出现的就是列表了,那么我们这里就拿RecyclerView作为代表进行演示。

RecyclerView的好处就不多说了,已经完全代替了之前的ListViewGridView,用法也就不赘述了,这里主要介绍一下适配器的编写。虽然网上有很多大神已经帮我们创建了各种通用的adapter,不过作为入门,我们还是要学习一下使用dataBinding创建adapter.

先来个简单的XML文件:

<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <import type="com.github.markzhai.sample.Person"/>
        <variable
            name="person"
            type="Person"/>
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="24sp"
            android:padding="5dp"
            android:textColor="#f0f"
            android:text='@{"姓名:" + person.name, default="aaa"}'/>
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="15sp"
            android:padding="2dp"
            android:textColor="#090"
            android:gravity="right"
            android:layout_marginRight="80dp"
            android:text='@{"年龄:" + person.age, default=12}'/>
    </LinearLayout>
</layout>

至于ViewModel就是一个简单的Person类,拥有nameage两个属性,接下来就是adapte的编写。

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {

    private List<Person> mList;    
    public MyAdapter() {
        mList = new ArrayList<>();
    }
    public void setData(List<Person> persons) {
        this.mList = persons;
    }

    @Override
    public MyAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        ItemRecyclerBinding itemBinding = 
                    DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()),
                                                                  R.layout.item_recycler,
                                                                  parent,
                                                                  false);

        return new ViewHolder(itemBinding);
    }

    @Override
    public void onBindViewHolder(MyAdapter.ViewHolder holder, int position) {
        holder.bind(mList.get(position));
    }

    @Override
    public int getItemCount() {
        return mList.size();
    }

    public static class ViewHolder extends RecyclerView.ViewHolder {

        final ItemRecyclerBinding itemBinding;

        public ViewHolder(ItemRecyclerBinding binding) {
            super(binding.getRoot());
            this.itemBinding = binding;
        }

        void bind(Person person) {
            itemBinding.setPerson(person);
        }
    }
}

通过DataBindingUtil.inflate()创建item布局,在ViewHolder中进行数据的绑定,这个时候,当数据源变化的时候,RecyclerView中的数据也跟着变化。

至于item的点击事件可以上面的onClick写法:

创建Presenter处理点击事件:

public static class Presenter{
    ItemRecyclerBinding mBinding;
    public Presenter(ItemRecyclerBinding binding){
        this.mBinding = binding;
    }
    public void onItemClick(Person person){
        Log.i("xwz--->", "name: " + person.getName() + "\tage: " + person.getAge());
        Toast.makeText(mBinding.getRoot().getContext(), "name: " + person.getName() + "\tage: " + person.getAge(), Toast.LENGTH_SHORT).show();
    }
}

在之前的bind方法中进行绑定:

void bind(Person person) {
    itemBinding.setPerson(person);
    itemBinding.setPresenter(new Presenter(itemBinding));
}

XML中设置点击事件即可:

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:onClick="@{() -> presenter.onItemClick(person)}"
    android:orientation="vertical">
...
</LinearLayout>

这里就可以直接将数据直接回传。

看一下运行效果:

RecyclerView的使用.gif

一些细节

databinding支持一些java的表达式

  • + - * / %
  • 字符串的连接"a"+"b"
  • 逻辑和位运算&& || & |
  • 一元运算+ - ! ~
  • 移位 >> >>> <<
  • 比较 == > < >= <=
  • instance of
  • 支持数据类型:character,String,numeric,null
  • 强转cast
  • 方法的调用
  • 成员变量的访问
  • 数组访问
  • 三元表达式? :

简单例子:

android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'

dataBinding不支持的Java特性

  • this
  • super
  • new
  • 泛型

dataBinding判空处理

使用??来进行判空操作

android:text="@{user.displayName ?? user.lastName}"

如果不为空则选择左侧值,否则选择右侧值,类似于:

android:text="@{user.displayName != null ? user.displayName : user.lastName}"

支持数组,集合,map

<data>
    <import type="android.util.SparseArray"/>
    <import type="java.util.Map"/>
    <import type="java.util.List"/>
    <variable name="list" type="List<String>"/>
    <variable name="sparse" type="SparseArray<String>"/>
    <variable name="map" type="Map<String, String>"/>
    <variable name="index" type="int"/>
    <variable name="key" type="String"/>
</data>
…
android:text="@{list[index]}"
…
android:text="@{sparse[index]}"
…
android:text="@{map[key]}"

资源的访问

dataBinding支持一般语法对资源的访问:

android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"

小结

dataBinding主要的作用就在于减少ActivityFragment层的代码,不再使用findViewById,让XML从之前只用于显示视图,到现在可以做一些操作。在性能上更是有很大的提高,内部采用0反射,使用位标记检测需要更新的view,每次数据的改变是在下一帧开始改变等等。

当然也有一些不足之处,Android StudioIDE支持还不是那么完善,在XML中一些方法不能智能生成和跳转,还有就是报错的错误信息,有的时候并不能定位到准确的位置。不过总体上来说dataBinding带来的好处远远的超过这些不足,所以还没有尝试的小伙伴,不妨试一试,相信你会爱上他的。

感谢dataBinding视频
markzhai
官方地址

我的博客


赏我 e(=2.72) 元咖啡钱吧,您的支持将鼓励我继续创作!



文章目录
  1. 1. MVVM介绍
  2. 2. 初步使用
    1. 2.1. 1. module的build.gradle文件加上一行配置代码
    2. 2.2. 2. 创建布局文件
    3. 2.3. 3. Activity中替换原来的setContentView()代码
    4. 2.4. 4. 接下来就是关键的ViewModel层
      1. 2.4.0.1. a. 单向绑定
      2. 2.4.0.2. b. 双向绑定
  • 3. 其他使用
    1. 3.0.1. 1. 事件处理
    2. 3.0.2. 2. ViewStub和include
    3. 3.0.3. 3. BindingAdapter的使用
    4. 3.0.4. 4. @BindingConversion
    5. 3.0.5. 5. DataBindingComponent
  • 4. RecyclerView中的应用
  • 5. 一些细节
    1. 5.0.1. databinding支持一些java的表达式
    2. 5.0.2. dataBinding不支持的Java特性
    3. 5.0.3. dataBinding判空处理
    4. 5.0.4. 支持数组,集合,map
    5. 5.0.5. 资源的访问
  • 6. 小结
  • 您是第 位小伙伴 | 本站总访问量 | 已经写了 83.0k 字啦

    载入天数...载入时分秒...