Data Binding 系列(三)布局和绑定表达式

3,829 阅读8分钟

这种表达式语言(expression language)使我们可以使用表达式处理 view 的事件。Data Binding 库会自动生成绑定类(binding class)用来处理 view 和 data 的绑定关系。

使用 Data Binding 的布局文件和传统的布局文件稍有不同,它的根标签是 layout,里面会有一个 data 子标签和一个根 view 子标签。这个根 view 子标签和传统的布局文件是一样的。具体如下所示:

<?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>

data 标签中声明的 user 变量,将会在绑定表达式中用到。

<variable name="user" type="com.example.User" />

绑定表达式用于为属性赋值,它使用的语法是 @{}。在下面的例子中,TextView 控件的属性 text ,被赋值为 user 变量的 firstName 属性值:

<TextView android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:text="@{user.firstName}" />

数据源

假设现在有一个描述 User 实体的数据对象:

public class User {
  public final String firstName;
  public final String lastName;
  public User(String firstName, String lastName) {
      this.firstName = firstName;
      this.lastName = lastName;
  }
}

这个数据类的成员属性都是不可变的、是 public 的。它还有另外一种写法,成员属性是 private 的,并且提供访问它们的方法:

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;
  }
}

从数据绑定的角度来看,上面两个实体类是等价的。用于给 android:text 属性赋值的表达式 @{user.firstName} ,会自动读取前一个类的 firstName 属性,或者调用后一个类的 getFirstName() 方法。而且,如果 firstName() 方法存在,也会调用这个方法。

可绑定的数据(Binding data

每个布局文件都会生成一个对应的绑定类。默认的绑定类的名字是文件名转为驼峰写法并加上后缀 Binding。比如文件名是 activity_main.xml,对应的绑定类名为 ActivityMainBinding。这个绑定类保存了数据变量和 view 属性的绑定关系,并且知道如何为 view 属性赋值。推荐的方式是在加载布局的时候创建绑定类,如下所示:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

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

    binding.user = User("Test", "User")
}

另外,还可以使用 inflate() 方法创建绑定类:

val binding: ActivityMainBinding = ActivityMainBinding.inflate(getLayoutInflater())

如果是在 FragmentListViewRecyclerView 中使用 Data Binding,你可能更倾向于使用 inflate() 方法或 DataBindingUtil 类,如下所示:

val listItemBinding = ListItemBinding.inflate(layoutInflater, viewGroup, false)
// or
val listItemBinding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false)

表达式语言(expression language

1 常用特性

表达式语言看起来很像代码里面的表达式。你可以在表达式中使用如下的操作符和关键字:

  • 计算相关运算符 + - / * %
  • 字符连接符 +
  • 逻辑运算符 && ||
  • 二元运算符 & | ^
  • 一元运算符 + - ! ~
  • 移位运算符 >> >>> <<
  • 比较运算符 == > < >= <=(注:< 运算符需要写成 &lt;
  • instance of
  • ()
  • 字符、字符串、数字、null
  • 强转
  • 方法调用
  • 属性访问
  • 数组访问 []
  • 三元元素符 ?:

举例:

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

2 不可用操作

下面操作只能在代码里使用,不能用于表达式语法:

  • this
  • super
  • new
  • 泛型声明

3 空结合运算符(??

空结合运算符左边表达式如果不为空,使用左边表达式结果,否则使用右边表达式结果

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

它等价于:

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

4 属性引用

使用绑定表达式,可以引用一个类的成员变量、get 方法、ObservableField

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

5 避免空指针异常

生成的绑定类会自动判空从而避免空指针异常。比如,表达式 @{user.name} ,如果 user 是空,user.name 会被赋予默认值 null。如果引用的是 user.ageage 的类型是 int,那么会使用 0 作为默认值。

6 集合

常用的集合,如数组、list、sparse list、map 等,可以方便地使用 [] 操作符访问它们的元素。

<data>
    <import type="android.util.SparseArray"/>
    <import type="java.util.Map"/>
    <import type="java.util.List"/>
    <variable name="list" type="List&lt;String>"/>
    <variable name="sparse" type="SparseArray&lt;String>"/>
    <variable name="map" type="Map&lt;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]}"

注:为了能正确解析 XML,需要将 < 替换为 &lt;。如 List<String> 应该写成 List&lt;String>

访问 map 中的元素,除了可以使用 @{map[key]},也可以使用 @{map.key}

7 字符串的写法

可以使用单引号包裹属性,在表达式中使用双引号,如下所示:

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

也可以使用双引号包裹属性,在表达式中使用单引号,如下所示:

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

8 访问资源

可以在表达式中使用如下语法访问资源:

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

9 事件处理

Data Binding 可以让我们在表达式中处理 view 分发的事件,比如 onClick()。属性的名字取决于监听器方法的名字,比如,View.OnClickListener 有一个 onClick() 方法,所以对应的属性名就是 android:onClick

处理 view 事件有两种方法:

  • 方法引用:在绑定表达式中,可以引用一个方法处理事件,需要注意的是,这个方法需要和监听器的方法签名完全一致。如果绑定表达式的结果是 null,对应的 view 不会设置监听器。
  • 监听器绑定(listener binding):这种方式总是会给对应的 view 设置监听器,当 view 收到事件时会调用 lambda 表达式。
  • 方法引用

事件可以和方法直接绑定,这种方式与 android:onClick 可以和 activity 中一个方法绑定很类似。这种方式的优势是,绑定表达式是在编译期间处理的,如果方法不存在或者签名不匹配,会直接报错。

和监听器绑定不同的是,方法引用的方式会在数据绑定的时候创建监听器,监听器绑定则是在收到事件的时候创建监听器。

示例如下:

class MyHandlers {
    fun onClickFriend(view: View) { ... }
}
<?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>

注: 表达式中的方法签名必须与监听器中的方法签名一致。

  • 监听器绑定(listener binding

监听器绑定的方式,只有在收到事件的时候才会执行。它和方法引用很像,但是它可以使用任意的表达式。这个特性在 Gradle 2.0 及以后可以使用。

方法引用的方式,要求方法的签名必须和监听器的方法签名一致。但是监听器绑定的方式,只要求返回值一致即可。例如,假如下面的 Presenter 类有一个 onSaveClick() 方法:

class Presenter {
fun onSaveClick(task: Task){}
}

onSaveClick() 可以与 android:onClick 绑定,如下所示:

<?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>

在上面这个例子中,我们没有定义 view 参数。监听器绑定提供了两种选择:要么忽略所有参数,要么显式写出所有参数。如果你喜欢写出参数,如下所示:

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

如果你需要使用这些参数,如下所示:

class Presenter {
    fun onSaveClick(view: View, task: Task){}
}
android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}"

同时,也可以有多个参数,如下所示:

class Presenter {
    fun onCompletedChanged(task: Task, completed: Boolean){}
}
<CheckBox android:layout_width="wrap_content" android:layout_height="wrap_content"
      android:onCheckedChanged="@{(cb, isChecked) -> presenter.completeChanged(task, isChecked)}" />

此外,如果你监听的事件返回值不是 void,那么你的表达式也需要返回相同的返回值。如下所示:

class Presenter {
    fun onLongClick(view: View, task: Task): Boolean { }
}
android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"

如果表达式不能正常执行,那么会返回默认值,引用类型返回 null,int 类型返回 0,布尔类型返回 false。

Imports、variables、includes

Data Binding 提供了诸如 importsvariablesincludes 等特性。imports 用于导入所需要的类,方便引用;variable 用于定义一个变量,方便在绑定表达式中使用。includes 使我们可以复用布局。

  • Imposts

下面这个例子展示了导入 View 这个类到布局文件中:

<data>
    <import type="android.view.View"/>
</data>

导入的目的就是为了方便在绑定表达式中使用。下面的例子展示了在表达式中引用 View 类的两个常量 VISIBLEGOEN

<TextView
   android:text="@{user.lastName}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
    • 类型别名

如果导入的两个类名字相同,可以为其中一个或两个起别名以区分:

<import type="android.view.View"/>
<import type="com.example.real.estate.View"
        alias="Vista"/>

注:java.lang.* 下面的类会自动导入。

  • Variables

变量的声明用于在绑定表达式中使用,如下所示,声明了 userimagenote 三个变量:

<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>

自动生成的绑定类,包含了这些变量的 get 和 set 方法。这些变量都会有默认值,引用类型默认值是 null,int 类型默认值是 0,布尔类型默认值是 false。

  • Includes

变量可以传递给 include 布局,如下所示,user 变量传递给了 name.xmlcontact.xml 两个布局:

<?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 binding 不支持 merge 直接作为一个根布局,如下所示是不支持的:

<?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><!-- Doesn't work -->
       <include layout="@layout/name"
           bind:user="@{user}"/>
       <include layout="@layout/contact"
           bind:user="@{user}"/>
   </merge>
</layout>