Data Binding 数据绑定(一)

前几天在忙一些其他的东西,DataBinding 这个系列的博客本应该在五月月初就要写的,结果一直拖到了现在,罪过罪过。

在学习 DataBinding 的过程中,参考 Google 官方的 DataBinding 示例 Demo,自己写了一个 DataBindingPractice Demo,用于练手。整个工程采用 MVP 架构 + DataBinding,欢迎 star、fork 和沟通交流。

本文介绍了 Data Binding 的一些基本概念和基本用法,主要包括以下三部分内容:

  1. Data Binding 的介绍
  2. Data Binding 中的布局文件
  3. Data Binding 中的事件处理
  4. Data Binding 中的布局详情

Data Binding 的介绍

简介

在 Data Binding 库之前,我们经常会写一些重复性很高而且毫无营养的代码,比如:findViewById()setText()setOnClickListener() 等。使用 Data Binding 库以后,可以使用声明式布局文件来减少粘结业务逻辑和布局文件的胶水代码。

  1. Data Binding 具有良好的灵活性和兼容性,它是一个 support 库,向后兼容至 Android 2.1(API Level 7+)。

  2. 若使用 Android Studio 开发环境开发 Android 应用程序,则必须满足以下两个条件才可以使用 Data Binding 库:

    • Gradle Plugin 版本必须是 1.5.0-alpha1 或以上的版本
    • Android Studio 的版本必须是1.3或以上的版本。

Data Binding 环境构建

在 Module 的 build.gradle 中添加如下代码,这样应用就支持 Data Binding 库了。

1
2
3
4
5
6
android {
....
dataBinding {
enabled = true
}
}

注意:若 app Module 依赖了一个使用 Data Binding 的库,则 app Module 的 build.gradle 也必须配置 Data Binding 库。

Data Binding 中的布局文件

第一个 data binding 表达式

与传统的布局文件相比,data binding 布局文件与其只有轻微的不同,data binding 布局文件中的根元素是 <layout> 标签,其中包含一个 <data> 标签和一个 <view> 标签,这个 <view> 标签的内容与普通布局文件的内容相同。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="user" type="com.example.User"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}"/>
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.lastName}"/>
</LinearLayout>
</layout>

  1. <data> 标签中的 user 变量是一个在这个 data binding 布局文件中会用到的属性。
  2. 在 data binding 布局文件中,data binding 表达式使用 @{} 语法。

数据对象(Data Object)

  1. 假设有一个 User 类是 plain-old Java object (POJO) 类型的,如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    public class User {
    public final String firstName;
    public final String lastName;
    public User(String firstName, String lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
    }
    }
  2. 还有一个 User 类,是 JavaBeans 类型的,如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class User {
    private final String firstName;
    private final String lastName;
    public User(String firstName, String lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
    }
    public String getFirstName() {
    return this.firstName;
    }
    public String getLastName() {
    return this.lastName;
    }
    }
  3. 这两个 User 类对于 Data Binding 库来说是等价的。在 TextView 中的 android:text 属性 @{user.firstName} 会使用 POJO User类中的 firstName 字段,或者 JavaBeans User 类中的 getFirstName() 方法。

绑定对象(Binding Data)

  1. 默认情况下,基于 data binding 布局文件会生成一个 Binding 类,此 Binding 类是将布局文件的名称转换成帕斯卡命名,并在之后接上 Binding 命名的。比如,布局文件名称是 activity_main.xml,则其对应的 Binding 类是 ActivityMainBinding。这个 Binding 类包含了布局文件中所有的布局属性和布局视图的绑定关系,并且知道如何向 data binding 表达式赋值。在 inflate 的时候,是创建 binding 关系最简单的时候,如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
    // 下面这行生成 Binding 类的代码和上面这行生成 Binding 类的代码是等价的。
    // MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater());
    User user = new User("Test", "User");
    binding.setUser(user);
    }
  2. 如果在 ListView 或者 RecyclerView 中的 Item 中使用 Data Binding,可以使用如下方式生成每个 Item 对应的 Binding 类,如下所示:

    1
    2
    3
    ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
    // or
    ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);

Data Binding 中的事件处理

Data Binding 库允许使用 data binding 表达式处理由 View 分发的事件。事件属性的名字由 Listener 中的方法名称决定。例如,在 View.onLongClickListener() 中有一个 onLongClick() 方法,则这个事件对应的属性名称是 android:onLongClick。有两种方法处理一个事件:

  • 方法引用:在表达式中,可以引用符合监听器方法签名的处理方法。
  • 监听绑定:在表达式中,是使用一个 Lambda 表达式处理事件的。

两者的区别,官方说法:方法引用和监听绑定的主要区别是,方法引用中监听器的实现是在数据绑定期间完成的,而不是在触发事件时创建的。如果更偏向于在事件发生时再计算表达式的值,则应该使用监听绑定。可以理解为:方法引用是在编译期处理,而监听绑定是在事件分发时处理。

方法引用(Method References)

  1. 在方法引用中,可以直接将事件绑定到一个处理类的方法上去,类似于 android:onClick 可以指定到一个 Activity 中的方法。和 View.onClick 相比,方法引用表达式的一个主要优点是:方法引用是在编译期处理的,所以如果引用的方法不存在或者方法的签名不匹配的话,在编译期就会报错。

  2. 若要将一个事件指派给一个处理类,则需要使用一个正常的 data binding 表达式,这个 data binding 表达式的值是将要调用的方法的名称。例如,有一个类如下所示:

    1
    2
    3
    public class MyHandlers {
    public void onClickFriend(View view) { ... }
    }

    data binding 表达式可以为 View 指定点击监听器,如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
    <variable name="handlers" type="com.example.MyHandlers"/>
    <variable name="user" type="com.example.User"/>
    </data>
    <LinearLayout
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{user.firstName}"
    android:onClick="@{handlers::onClickFriend}"/>
    </LinearLayout>
    </layout>

    注意:在 data binding 表达式中的方法签名必须和监听器对象中的方法签名匹配,引用的方法的参数必须事件监听器的方法的参数匹配。

监听绑定(Listener Bindings)

监听绑定是在事件发生时才会运行的 data binding 表达式。

  • 监听绑定在 Gradle Plugin 2.0及更新的版本上才可以使用
  • 和方法引用类似,不过它允许你运行任意数据绑定表达式(不限制处理方法的参数)
  • 在监听绑定中,引用的方法的返回值和事件监听器期望的返回值匹配即可(除非它期望是void的)
  1. 例如,有一个 Presenter 类如下所示:

    1
    2
    3
    public class Presenter {
    public void onSaveClick(Task task){}
    }

    可以将点击事件绑定到这个 presenter 类上,如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
    <variable name="task" type="com.android.example.Task" />
    <variable name="presenter" type="com.android.example.Presenter" />
    </data>
    <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:onClick="@{() -> presenter.onSaveClick(task)}" />
    </LinearLayout>
    </layout>
    • 监听器由 Lambda 语句表达,并且只允许作为表达式的根元素使用。
    • 如果在表达式中会使用一个回调,Data Binding会自动的创建必要的监听器并将其注册到对应的事件。当该控件的事件发生时,Data Binding会计算表达式的值。
    • 在常规的绑定表达式中,当监听器的表达式计算式,Data Binding会保证绑定表达式中引用变量的空值安全性和线程安全性。
    • 请注意,在上面的例子中,没有定义传递进 onClick() 中的 View 参数。监听绑定为监听器的参数提供了两种选择:要么把参数全部写上,要么把参数全部忽略不写。如果倾向于写出全部的参数,则上面的例子该像下面这样写:

      1
      android:onClick="@{(view) -> presenter.onSaveClick(task)}"

      如果想在表达式中使用参数,则可以像下面代码一样使用:

      1
      2
      3
      4
      5
      public class Presenter {
      public void onSaveClick(View view, Task task){}
      }
      android:onClick="@{(view) -> presenter.onSaveClick(view, task)}"
  2. 还可以使用具有多个参数的 Lambda 表达式:

    1
    2
    3
    4
    5
    6
    7
    public class Presenter {
    public void onCompletedChanged(Task task, boolean completed){}
    }
    <CheckBox
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:onCheckedChanged="@{(cb, isChecked) -> presenter.completeChanged(task, isChecked)}" />
  3. 如果正在监听的事件函数的返回值是非空的,则绑定表达式的值也必须返回相同类型的值。例如,正在监听 onLongClick() 事件函数,则绑定表达式需要返回 boolean 型的。如下所示:

    1
    2
    3
    4
    5
    public class Presenter {
    public boolean onLongClick(View view, Task task){}
    }
    android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"
    • 如果由于空对象而无法计算绑定表达式的值,则 Data Binding 返回 Java 中默认的值,例如:引用型对象则返回 null, int 型则返回0,Boolean 型则返回 false 等等。
    • 如果需要使用带断言(例如三元表达式)的表达式,则可以使用void作为空操作符号。例如:
      1
      android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"

避免复杂的监听

  1. 监听器表达式是非常强大的,会让你的代码非常容易阅读。
  2. 另一方面,如果监听器中包含复杂的表达式则会让布局文件难以阅读和维护,所以布局文件中的表达式应尽可能简单,表达式只是调用回调方法,而具体的业务逻辑应该写在回调方法中。
  3. 有个别点击事件的监听器回调函数的方法名称和 View 的 android:onClick 相同,下面有一些新的属性名称,用于避免冲突:
Class Listener Setter Attribute
SearchView setOnSearchClickListener(View.OnClickListener) android:onSearchClick
ZoomControls setOnZoomInClickListener(View.OnClickListener) android:onZoomIn
ZoomControls setOnZoomOutClickListener(View.OnClickListener) android:onZoomOut

Data Binding 中的布局详情

导入(Imports)

  1. 在布局文件中的 data 标签中可以使用 import 标签导入类,就像在 java 文件中导入类一样,如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <data>
    <import type="android.view.View"/>
    </data>
    <TextView
    android:text="@{user.lastName}"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
  2. 当导入的类名冲突时,可以使用 alias 属性为类起个别名,如下所示:

    1
    2
    3
    <import type="android.view.View"/>
    <import type="com.example.real.estate.View"
    alias="Vista"/>
    • 别名只在此布局文件内有效。导入的类,可以在 data binding 表达式中使用,也可以在申明变量时使用。
    • 目前,在 Android Studio 中,并没有提供 Data Binding 在布局文件中导入类自动补全的功能。如果在布局文件中使用的类,没有被导入,编译可以正常通过,但是运行时会出现问题。可以通过在申明变量时,使用完全限定名类避免这个问题隐患。
  3. 在 data binding 表达式中可以使用导入类的静态方法和静态字段,如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <data>
    <import type="com.example.MyStringUtils"/>
    <variable name="user" type="com.example.User"/>
    </data>
    <TextView
    android:text="@{MyStringUtils.capitalize(user.lastName)}"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>
  4. 和在 Java 中一样,java.lang.* 包下的类会被自动导入。

变量(Variables)

  1. data 标签中,可以定义任意数量的变量,每个变量都可以被该布局文件中的任意一个 data binding 表达式使用。如下所示:

    1
    2
    3
    4
    5
    6
    <data>
    <import type="android.graphics.drawable.Drawable"/>
    <variable name="user" type="com.example.User"/>
    <variable name="image" type="Drawable"/>
    <variable name="note" type="String"/>
    </data>
  2. Data Binding 在编译期内会对申明的变量检查类型,如果该变量实现了 Observable 接口,或者是一个 Observable 集合类型的,那它应该在类型上反映出来。若是一个没有实现 Observable 的基础类或者基础接口,则该类不会被观察。

  3. 自动生成的 binding 类会为每个变量生成对应的 setter 和 getter 方法,在每个变量的 setter 方法被调用之前,该变量将会采用默认值,即:引用型变量默认值是null, int 型变量默认值是0,boolean 型变量的默认值是 false。
  4. Data Binding 会生成一个特殊的名为 context 的变量,以便 data binding 表达式在需要的时候使用,此 context 变量的值其实就是该布局文件中 rootViewgetContext() 的返回值。
  5. 如果在该布局文件中有一个名为 context 的变量,则 Data Binding 生成的 context 将会被覆盖。

自定义 Binding 类的名称(Custom Binding Class Names)

  1. Data Binding 会为每一个使用了 Data Binding 的布局文件生成一个对应的 Binding 类,该类的名称是基于布局文件的名称的,采用大驼峰命名规则,移除下划线_,并在最后追加 Binding,这个类会被放在该 Module 包的 databinding 包中。例如:如果一个名为 activity_main.xml 的布局文件使用了 Data Binding 库,则 Data Binding 库会自动生成一个名为ActivityMainBinding 的 Binding 类,如果该 Module 的包名为 com.lijiankun24.databindingpractice,则 ActivityMainBinding 类在 com.lijiankun24.databindingpractice.databinding 包下。
  2. 通过修改 data 标签的 class 属性,就可以修改 binding 类的名称和位置。

    • 若像下面这样:

      1
      2
      3
      <data class="ActivityCustomBinding">
      ...
      </data>

      则该布局文件对应的 Binding 类的名称是 ActivityCustomBinding,而与该布局文件的名称无关。

    • 若像下面这样:

      1
      2
      3
      <data class=".ActivityCustomBinding">
      ...
      </data>

      则该布局文件对应的 Binding 类会被放在该 Module 包下,而不是该 Module 的 databinding 包下。

    • 若像下面这样:

      1
      2
      3
      <data class="com.example.ActivityCustomBinding">
      ...
      </data>

      则可以任意地指定该布局文件对应的 Binding 类所在的位置。

Includes 标签

  1. data 标签中声明的变量,可以通过应用命名空间将该变量传递到被 include 的布局中。如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:bind="http://schemas.android.com/apk/res-auto">
    <data>
    <variable name="user" type="com.example.User"/>
    </data>
    <LinearLayout
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <include layout="@layout/name"
    bind:user="@{user}"/>
    <include layout="@layout/contact"
    bind:user="@{user}"/>
    </LinearLayout>
    </layout>

    如上面代码所示,可以将在 data 标签中声明的 user 变量传递到name.xmlcontact.xml 布局文件中,前提是在这两个布局文件中必须也声明了 user 变量。

  2. Data Binding 库并不支持 merge 标签直接做为其子元素,如下所示的代码是不允许的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:bind="http://schemas.android.com/apk/res-auto">
    <data>
    <variable name="user" type="com.example.User"/>
    </data>
    <merge>
    <include layout="@layout/name"
    bind:user="@{user}"/>
    <include layout="@layout/contact"
    bind:user="@{user}"/>
    </merge>
    </layout>

表达式语法(Expression Language)

通用特性

表达式语言和 Java 表达式有很多相似之处,如下所示:

  • 数学运算符:+ - * / %
  • 字符串连接符:+
  • 逻辑运算符:&& ||
  • 位运算符:& | ^
  • 一元操作符:+ - ! ~
  • 移位运算: >> >>> <<
  • 比较运算符:== > < >= <=
  • 实例判断:instanceof
  • 组:()
  • Literals - character, String, numeric, null
  • 类型转换 Cast
  • 方法调用 Method calls
  • 字段存取 Field access
  • 数组存取 Array access: []
  • 三目运算符:?:
    如:
    1
    2
    3
    android:text="@{String.valueOf(index + 1)}"
    android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
    android:transitionName='@{"image_" + id}'

不支持的操作符

一些在 Java 中的操作符,在 Binding 表达式中不支持,如下:

  • this
  • super
  • new
  • 显式泛型调用

空合并运算符(Null Coalescing Operator)

空合并运算符(??): 如果左操作数不为空,则选择左操作数否则选择右操作数。

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

上面代码等价于:

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

空指针异常处理(Avoiding NullPointerException)

Data Binding 生成的代码中会自动检查 null 并避免空指针异常。例如,在 data binding 表达式 @{user.name} 中,如果 user 变量是 null 的,若 name 是 String 类型的,则将为 user.name 分配其默认值 null;若引用了 user.age,其中 age 是 int 型的,那么它的默认值是0。

集合(Collections)

可以使用 [] 操作符来操作通用的容器类,比如:arrays, lists, sparse lists 和 maps,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<data>
<import type="android.util.SparseArray"/>
<import type="java.util.Map"/>
<import type="java.util.List"/>
<variable name="list" type="List&lt;String&gt;"/>
<variable name="sparse" type="SparseArray&lt;String&gt;"/>
<variable name="map" type="Map&lt;String, String&gt;"/>
<variable name="index" type="int"/>
<variable name="key" type="String"/>
</data>
android:text="@{list[index]}"
android:text="@{sparse[index]}"
android:text="@{map[key]}"

字符串语法(String Literals)

  1. 当属性值使用单引号括起来时,在表达式中需要使用双引号。

    1
    android:text='@{map["firstName"]}'
  2. 属性值也可以使用双引号括起来,则表达式中的字符串应该使用 ‘ 或者后引号 ` ,如:

    1
    2
    android:text="@{map[`firstName`}"
    android:text="@{map['firstName']}"

资源(Resources)

  1. 在表达式中可以使用正常的语法引用资源。

    1
    android:padding="@{large ? @dimen/largePadding : @dimen/smallPadding}"
  2. 在字符串格式化和复数形式中可以使用参数,如:

    1
    2
    android:text="@{@string/nameFormat(firstName, lastName)}"
    android:text="@{@plurals/banana(bananaCount)}"
  3. 当复数形式中有多个参数是,多个参数必须同时传递进去,如:

    1
    2
    3
    4
    Have an orange
    Have %d oranges
    android:text="@{@plurals/orange(orangeCount, orangeCount)}"
  4. 一些资源需要在表达式中使用特定引用类型,如:

Type Normal Reference Expression Reference
String[] @array @stringArray
int[] @array @intArray
TypedArray @array @typedArray
Animator @animator @animator
StateListAnimator @animator @stateListAnimator
color int @color @color
ColorStateList @color @colorStateList

DataBinding 第一篇文章先介绍这些,如果有什么问题欢迎指出。我的工作邮箱:jiankunli24@gmail.com


参考资料:

DataBInding 官方文档

深入Android Data Binding(一):使用详解 – YamLee

Android Data Binding 系列(一) – 详细介绍与使用 – ConnorLin

DataBinding(一)-初识 – sakasa

(译)Data Binding 指南 – 杨辉