Data Binding - xiaoniudonghe2015/Android-Java-Code-Style GitHub Wiki

什么是Data Binding

Data Binding,顾名思义,数据绑定,是Google对MVVM在Android上的一种实现,可以直接绑定数据到xml中,并实现自动刷新。还支持双向绑定,尽管使用场景不是那么多。 Data Binding可以提升开发效率(节省很多以往需要手写的java代码),性能高(甚至超越手写代码),功能强(强大的表达式支持)。

用途

去掉Activities & Fragments内的大部分UI代码(setOnClickListener, setText, findViewById, etc.)

XML变成UI的唯一真实来源

减少定义view id的主要用途(数据绑定直接发生在xml)

优势

UI代码放到了xml中,布局和数据更紧密

性能超过手写代码

保证执行在主线程

劣势

IDE支持还不那么完善(提示、表达式)

报错信息不那么直接

重构支持不好(xml中进行重构,java代码不会自动修改)

使用

使用起来实在很简单,在app模块的build.gradle中加上几行代码就行了。

Gradle

android {
    …
    dataBinding {
        enabled = true
    }
}

layout tag

把一个普通的layout变成data binding layout也只要几行的修改:

<layout>
 // 原来的layout
</layout>

在xml的最外层套上layout标签即可,修改后就可以看到生成了该布局对应的*Binding类。

Binding生成规则 默认生成规则:xml通过文件名生成,使用下划线分割大小写。 比如activity_demo.xml,则会生成ActivityDemoBinding,item_search_hotel则会生成ItemSearchHotelBinding。 view的生成规则类似,只是由于是类变量,首字母不是大写,比如有一个TextView的id是first_name,则会生成名为firstName的TextView。 我们也可以自定义生成的class名字,只需要:

<data class=“ContactItem”>
…
</data>

这样生成的类就会变成ContactItem。

基础用法

生成Binding实例

所有Binding实例的生成都可以通过DataBindingUtil进行,方法名与该view的原inflate方法一致,如activity仍然为setContentView,只是增加了参数因为需要获得activity。

 @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityDemoBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_demo);
    }

去除findViewById

使用了Data Binding后,我们再也不需要findViewById,因为一切有设置id的view,都已经在Binding类中被初始化完成了,只需要直接通过binding实例访问即可。没有设置id的view不会出现Binding类中,根布局除外.

变量绑定

使用data标签,我们就可以在xml中申明变量,在其中使用该变量的field,并通过binding实例set进来。 如:

<data>
    <variable
        name="employee"
        type="com.github.markzhai.databindingsample.Employee"/>
</data>
<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_horizontal"
    android:orientation="vertical"
    tools:context=".DemoActivity">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@{employee.lastName}"
        android:layout_marginLeft="5dp"/>
</LinearLayout>
public class Employee {

    private String mLastName;
    private String mFirstName;

    public Employee(String lastName, String firstName) {
        mLastName = lastName;
        mFirstName = firstName;
    }

    public Employee(String lastName, String firstName, boolean fired) {
        mLastName = lastName;
        mFirstName = firstName;
    }

    @Bindable
    public String getLastName() {
        return mLastName;
    }

    public void setLastName(String lastName) {
        mLastName = lastName;
    }

    @Bindable
    public String getFirstName() {
        return mFirstName;
    }

    public void setFirstName(String firstName) {
        mFirstName = firstName;
    }
}

然后我们就可以在java代码中使用

binding.setEmployee(employee);
// 或者直接通过setVariable
binding.setVariable(BR.employee, employee);

事件绑定

严格意义上来说,事件绑定也是一种变量绑定。我们可以在xml中直接绑定

android:onClick

android:onLongClick

android:onTextChanged

方法引用

不可以通过对象.变量.方法调用 通常会在java代码中定义一个名为Handler或者Presenter的类,然后set进来,方法签名需和对应listener方法一致。

<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:bind="http://schemas.android.com/apk/res-auto">
    <data>
        <import type="android.view.View"/>
        <variable
            name="employee"
            type="com.github.markzhai.databindingsample.Employee"/>
        <variable
            name="presenter"
            type="com.github.markzhai.databindingsample.DemoActivity.Presenter"/>
    </data>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center_horizontal"
        android:orientation="vertical"
        tools:context=".DemoActivity">
        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="输入 First Name"
            android:onTextChanged="@{presenter::onTextChanged}"/>
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{presenter.onClick}"
            android:text="@{employee.firstName}"/>
    </LinearLayout>
</layout>

在Java代码中:

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    binding.setPresenter(new Presenter());
    ...
}
public class Presenter {
    public void onTextChanged(CharSequence s, int start, int before, int count) {
        employee.setFirstName(s.toString());
        employee.setFired(!employee.isFired.get());
    }
    public void onClick(View view) {
        Toast.makeText(DemoActivity.this, "点到了", Toast.LENGTH_SHORT).show();
    }
}

监听器绑定(lambda)

可以不遵循默认的方法签名:

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_margin="5dp"
    android:visibility="@{employee.isFired ? View.GONE : View.VISIBLE}"
    android:onClick="@{() -> presenter.onClickListenerBinding(employee)}"/>
public class Presenter {
    public void onClickListenerBinding(Employee employee) {
        Toast.makeText(DemoActivity.this, employee.getLastName(),
                Toast.LENGTH_SHORT).show();
    }
}

性能

0反射

findViewById需要遍历整个viewgroup,而现在只需要做一次就可以初始化所有需要的view

使用位标记来检验更新(dirtyFlags)

数据改变在下一次批量更新才会触发操作

表达式缓存,同一次刷新中不会重复计算

进阶用法

表达式

算术 + - / * %

字符串合并 +

逻辑 && ||

二元 & | ^

一元 + - ! ~

移位 >> >>> <<

比较 == > < >= <=

Instanceof

Grouping ()

文字 - character, String, numeric, null

Cast

方法调用

Field 访问

Array 访问 []

三元 ?:

尚且不支持this, super, new, 以及显示的泛型调用。 值得一提的是还有空合并运算符,如

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

会取第一个非空值作为结果。

这里举一个常见的例子,某个view的margin是其左侧ImageView的margin加上该ImageView的宽度,以往我们可能需要再定义一个dimension来放这两个值的合,现在只需要

android:marginLeft="@{@dimen/margin + @dimen/avatar_size}"

就搞定了。

我们甚至还可以直接组合字符串,如:

android:text="@{@string/nameFormat(firstName, lastName)}"
<string name="nameFormat">%s, %s</string>

避免空指针

data binding会自动帮助我们进行空指针的避免,比如说@{employee.firstName},如果employee是null的话,employee.firstName则会被赋默认值(null)。int的话,则是0。

需要注意的是数组的越界,毕竟这儿是xml而不是java,没地方让你去判断size的。

include

<include layout=“@layout/namebind:user="@{user}"/>

对于include的布局,使用方法类似,不过需要在里面绑定两次,外面include该布局的layout使用bind:user给set进去。 这里需要注意的一点是,被include的布局必须顶层是一个ViewGroup,目前Data Binding的实现,如果该布局顶层是一个View,而不是ViewGroup的话,binding的下标会冲突(被覆盖),从而产生一些预料外的结果。

Observable

一个纯净的Java ViewModel类被更新后,并不会让UI去更新。而数据绑定后,我们当然会希望数据变更后UI会即时刷新,Observable就是为此而生的概念。 类继承BaseObservable:

private static class User extends BaseObservable {
   private String firstName;
   private String lastName;
   @Bindable
   public String getFirstName() {
       return this.firstName;
   }
   @Bindable
   public String getLastName() {
       return this.lastName;
   }
   public void setFirstName(String firstName) {
       this.firstName = firstName;
       notifyPropertyChanged(BR.firstName);
   }
   public void setLastName(String lastName) {
       this.lastName = lastName;
       notifyPropertyChanged(BR.lastName);
   }
}

BaseObservable提供了一系列notify函数(其实就是notifyChange和notifyPropertyChanged),前者会刷新所有的值域,后者则只更新对应BR的flag,该BR的生成通过注解@Bindable生成,在上面的实例代码中,我们可以看到两个get方法被注释上了,所以我们可以通过BR访问到它们并进行特定属性改变的notify。

Observable Fields

如果所有要绑定的都需要创建Observable类,那也太麻烦了。所以Data Binding还提供了一系列Observable,包括 ObservableBoolean, ObservableByte, ObservableChar, ObservableShort, ObservableInt, ObservableLong, ObservableFloat, ObservableDouble, 和ObservableParcelable。我们还能通过ObservableField泛型来申明其他类型,如:

private static class User {
   public final ObservableField<String> firstName =
       new ObservableField<>();
   public final ObservableField<String> lastName =
       new ObservableField<>();
   public final ObservableInt age = new ObservableInt();
}

而在xml中,使用方法和普通的String,int一样,只是会自动刷新,但在java中访问则会相对麻烦:

user.firstName.set("Google");
int age = user.age.get();

相对来说,每次要get/set还是挺麻烦,私以为还不如直接去继承BaseObservable。

Observable Collections

有一些应用使用更动态的结构来保存数据,这时候我们会希望使用Map来存储数据结构。Observable提供了ObservableArrayMap:

public class Employee extends BaseObservable {
    public ObservableArrayMap<String, String> user = new ObservableArrayMap<>();

    public Employee() {
        user.put("hello", "world");
        user.put("hi", "world");
        user.put("yo", "world");
    }
}

而在xml中,我们可以直接通过下标key访问它们:

 <data>
        <variable
            name="employee"
            type="com.github.markzhai.sample.Employee"/>
    </data>
…
<TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:text='@{employee.user["hello"]}'/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:text='@{employee.user["hi"]}'/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:text='@{employee.user["yo"]}'/>

当我们不想定义key的时候,可以使用ObservableArrayList:

ObservableArrayList<Object> user = new ObservableArrayList<>();
user.add("Google");
user.add("Inc.");
user.add(17);

layout中直接通过数字下标进行访问。


列表绑定

App中经常用到列表展示,Data Binding在列表中一样可以扮演重要的作用,直接绑定数据和事件到每一个列表的item。

RecyclerView

过去我们往往会使用ListView、GridView、或者GitHub上一些自定义的View来做瀑布流。自从RecyclerView出现后,我们有了新选择,只需要使用LayoutManager就可以。RecyclerView内置的垃圾回收,ViewHolder、ItemDecoration装饰器机制都让我们可以毫不犹豫地替换掉原来的ListView和GridView。

Generic Binding

我们只需要定义一个基类ViewHolder,就可以方便地使用上Data Binding:

public class BindingViewHolder<T extends ViewDataBinding> extends RecyclerView.ViewHolder {

    protected final T mBinding;

    public BindingViewHolder(T binding) {
        super(binding.getRoot());
        mBinding = binding;
    }

    public T getBinding() {
        return mBinding;
    }
}
 @Override
    public BindingViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        ViewDataBinding binding = DataBindingUtil.inflate(mLayoutInflater,
                    R.layout.item_employee, parent, false);
        return new BindingViewHolder(binding);
    }
public void onBindViewHolder(BindingHolder holder, int position) {
   final T item = mItems.get(position);
   holder.getBinding().setVariable(BR.item, item);
   holder.getBinding().executePendingBindings();
}

executePendingBindings会强制立即刷新绑定的改变。 Adapter可以直接使用该ViewHolder,或者再继承该ViewHolder,T使用具体Item的Binding类(以便直接访问内部的View)。至于Listener,可以在onBindViewHolder中进行绑定,做法类似于普通View。

Setter

就像Data Binding会自动去查找get方法一下,在遇到属性绑定的时候,它也会去自动寻找对应的set方法。 拿DrawerLayout举一个例子:

<android.support.v4.widget.DrawerLayout
    android:layout_width=“wrap_contentandroid:layout_height=“wrap_contentapp:scrimColor=“@{@color/scrimColor}”/>

如此,通过使用app命名空间,data binding就会去根据属性名字找对应的set方法,scrimColor -> setScrimColor:

public void setScrimColor(@ColorInt int color) {
    mScrimColor = color;
    invalidate();
}

如果找不到的话,就会在编译期报错。 利用这种特性,对一些第三方的自定义View,我们就可以继承它,来加上我们的set函数,以对其使用data binding。 比如Fresco的SimpleDraweeView,我们想要直接在xml指定url,就可以加上:

public void setUrl(String url) {
    view.setImageURI(TextUtils.isEmpty(url) ? null : Uri.parse(url));
}

这般,就能直接在xml中去绑定图片的url。这样是不是会比较麻烦呢,而且有一些系统的View,难道还要继承它们然后用自己实现的类?其实不然,我们还有其他方法可以做到自定义属性绑定。

BindingAdapter

如果没有对应的set方法,或者方法签名不同怎么办?BindingAdapter注释可以帮我们来做这个。 事实上这个Adapter已经由Data Binding实现好了,可以在android.databinding.adapters.ViewBindingAdapter看到有很多定义好的适配器,还有BindingMethod。如果需要自己再写点什么,仿照这些来写就好了。 我们还可以进行多属性绑定,比如

@BindingAdapter({"bind:imageUrl", "bind:error"})
public static void loadImage(ImageView view, String url, Drawable error) {
   Picasso.with(view.getContext()).load(url).error(error).into(view);
}
 <ImageView
            android:id="@+id/avatar"
            android:layout_width="150dp"
            android:layout_height="150dp"
            android:visibility="@{employee.isFired ? View.INVISIBLE : View.VISIBLE}"
            app:imageUrl="@{employee.avatar}"
            app:placeholder="@{@drawable/default_avatar}"/>

来使用Picasso读取图片到ImageView。

双向绑定

自定义Listener

过去,我们需要自己定义Listener来做双向绑定:

<EditText android:text=“@{user.name}”
    android:afterTextChanged=“@{callback.change}”/>
public void change(Editable s) {
    final String text = s.toString();
    if (!text.equals(name.get()) {
        name.set(text);
    }
}

需要自己绑定afterTextChanged方法,然后检测text是否有改变,有改变则去修改observable。

新方式 - @=

现在可以直接使用@=(而不是@)来进行双向绑定了,使用起来十分简单

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

    <data>

        <variable
            name="model"
            type="com.github.markzhai.sample.FormModel"/>
    </data>


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

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

        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:inputType="textPassword"
            android:text="@={model.password}"/>

        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{@string/welcome(model.name)}"/>

    </LinearLayout>
</layout>
public class FormModel extends BaseObservable {

    private String mName;
    private String mPassword;

    public FormModel(String name, String password) {
        mName = name;
        mPassword = password;
    }

    @Bindable
    public String getName() {
        return mName;
    }

    public void setName(String name) {
        mName = name;
        notifyPropertyChanged(com.github.markzhai.sample.BR.name);
    }

    @Bindable
    public String getPassword() {
        return mPassword;
    }

    public void setPassword(String password) {
        mPassword = password;
        notifyPropertyChanged(com.github.markzhai.sample.BR.password);
    }
}

这样,我们对这个EditText的输入,就会自动set到对应model的name字段上。

表达式链

重复的表达式

<ImageView android:visibility=“@{user.isAdult ? View.VISIBLE : View.GONE}”/>
<TextView android:visibility=“@{user.isAdult ? View.VISIBLE : View.GONE}”/>
<CheckBox android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>

可以简化为:

<ImageView android:id=“@+id/avatarandroid:visibility=“@{user.isAdult ? View.VISIBLE : View.GONE}”/>
<TextView android:visibility=“@{avatar.visibility}”/>
<CheckBox android:visibility="@{avatar.visibility}"/>

隐式更新

<CheckBox android:id=”@+id/seeAds“/>
<ImageView android:visibility=“@{seeAds.checked ?
  View.VISIBLE : View.GONE}”/>

这样CheckBox的状态变更后ImageView会自动改变visibility。

Lambda表达式

除了直接使用方法引用,在Presenter中写和OnClickListener一样参数的方法,我们还能使用Lambda表达式:

android:onClick=“@{(view)->presenter.save(view, item)}”
android:onClick=“@{()->presenter.save(item)}”
android:onFocusChange=“@{(v, fcs)->presenter.refresh(item)}”

我们还可以在lambda表达式引用view id(像上面表达式链那样),以及context。

动画

transition

使用data binding后,我们还能自动去做transition动画:

binding.addOnRebindCallback(new OnRebindCallback() {
    @Override
    public boolean onPreBind(ViewDataBinding binding) {
        ViewGroup sceneRoot = (ViewGroup) binding.getRoot();
        TransitionManager.beginDelayedTransition(sceneRoot);
        return true;
    }
});

这样,当我们的view发生改变,比如visibility变化的时候,就能看到一些transition动画。

使用建议

对新项目,不要犹豫,直接上。

对于老的项目,可以替换ButterKnife这种库,从findViewById开始改造,逐渐替换老代码。

callback绑定只做事件传递,NO业务逻辑,比如转账

保持表达式简单(不要做过于复杂的字符串、函数调用操作)

对于老项目,可以进行以下的逐步替换:

Level 1 - No more findViewById

逐步替换findViewById,取而代之地,使用binding.name, binding.age直接访问View。

Level 2 - SetVariable

引入variable,把手动在代码对View进行set替换为xml直接引用variable。

Level 3 - Callback

使用Presenter/Handler类来做事件的绑定。

Level 4 - Observable

创建ViewModel类来进行即时的属性更新触发UI刷新。

Level 5 - 双向绑定

运用双向绑定来简化表单的逻辑,将form data变成ObservableField。这样我们还可以在xml做一些酷炫的事情,比如button仅在所有field非空才为enabled(而过去要做到这个得加上好几个EditText的OnTextChange监听)。

Demo源码:https://github.com/markzhai/DataBindingSample.git

⚠️ **GitHub.com Fallback** ⚠️