布局和绑定表达式

表达式语言允许您编写处理视图分派的事件的表达式。数据绑定库会自动生成将布局中的视图与您的数据对象绑定的所需类。

数据绑定布局文件略有不同,它们以 layout 根标签开头,后跟一个 data 元素和一个 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 文本设置为 user 变量的 firstName 属性

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

数据对象

假设您有一个普通对象来描述 User 实体

Kotlin

data class User(val firstName: String, val lastName: String)

Java

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

这种类型的对象具有永不更改的数据。在应用中,数据一次读取后永不更改是很常见的。也可以使用遵循一组约定的对象,例如在 Java 编程语言中使用访问器方法,如下例所示

Kotlin

// Not applicable in Kotlin.
data class User(val firstName: String, val lastName: String)

Java

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() 方法,它也会解析为该方法。

绑定数据

每个布局文件都会生成一个绑定类。默认情况下,类的名称基于布局文件的名称,转换为 Pascal 大小写,并添加 Binding 后缀。例如,前面的布局文件名是 activity_main.xml,因此相应的生成绑定类是 ActivityMainBinding

此绑定类包含从布局属性(例如 user 变量)到布局视图的所有绑定,并且知道如何为绑定表达式赋值。我们建议在加载布局时创建绑定,如下例所示

Kotlin

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

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

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

Java

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
   User user = new User("Test", "User");
   binding.setUser(user);
}

在运行时,应用会在 UI 中显示 Test 用户。或者,您可以使用 LayoutInflater 获取视图,如下例所示

Kotlin

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

Java

ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());

如果您在 FragmentListViewRecyclerView 适配器中使用数据绑定项,您可能更喜欢使用绑定类的 inflate() 方法或 DataBindingUtil 类,如以下代码示例所示

Kotlin

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

Java

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

表达式语言

常见特性

表达式语言与托管代码中的表达式非常相似。您可以在表达式语言中使用以下运算符和关键字

  • 算术运算符:+ - / * %
  • 字符串连接符:+
  • 逻辑运算符:&& ||
  • 二进制运算符:& | ^
  • 一元运算符:+ - ! ~
  • 移位运算符:>> >>> <<
  • 比较运算符:== > < >= <=< 需要转义为 &lt;
  • instanceof
  • 分组:()
  • 字面量,例如字符、字符串、数字、null
  • 强制转换
  • 方法调用
  • 字段访问
  • 数组访问:[]
  • 三元运算符:?:

以下是一些示例

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

缺少的操作

以下操作在托管代码中使用的表达式语法中缺失

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

空合并运算符

空合并运算符(??)在左操作数不为 null 时选择左操作数,如果左操作数为 null 则选择右操作数

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

这在功能上等同于以下代码

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

属性引用

表达式可以使用以下格式引用类中的属性,该格式适用于字段、getter 和 ObservableField 对象

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

避免空指针异常

生成的数据绑定代码会自动检查 null 值并避免空指针异常。例如,在表达式 @{user.name} 中,如果 user 为 null,则 user.name 会被赋予其默认值 null。如果您引用 user.age(其中 age 为 int 类型),则数据绑定会使用默认值 0

视图引用

表达式可以使用以下语法按 ID 引用布局中的其他视图

android:text="@{exampleText.text}"

在以下示例中,TextView 视图引用了同一布局中的 EditText 视图

<EditText
    android:id="@+id/example_text"
    android:layout_height="wrap_content"
    android:layout_width="match_parent"/>
<TextView
    android:id="@+id/example_output"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{exampleText.text}"/>

集合

为方便起见,您可以使用 [] 运算符访问常见的集合,例如数组、列表、稀疏列表和映射。

<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]}"

您还可以使用 object.key 表示法引用映射中的值。例如,您可以将上例中的 @{map[key]} 替换为 @{map.key}

字符串字面量

您可以使用单引号将属性值括起来,这样您就可以在表达式中使用双引号,如下例所示

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

您也可以使用双引号将属性值括起来。在这种情况下,字符串字面量必须用反引号 ` 括起来,如下所示

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

资源

表达式可以使用以下语法引用应用资源

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

您可以通过提供参数来评估格式字符串和复数

android:text="@{@string/nameFormat(firstName, lastName)}"
android:text="@{@plurals/banana(bananaCount)}"

您可以将属性引用视图引用作为资源参数传递

android:text="@{@string/example_resource(user.lastName, exampleText.text)}"

当一个复数需要多个参数时,传递所有参数


  Have an orange
  Have %d oranges

android:text="@{@plurals/orange(orangeCount, orangeCount)}"

某些资源需要显式类型评估,如下表所示

类型 普通引用 表达式引用
String[] @array @stringArray
int[] @array @intArray
TypedArray @array @typedArray
Animator @animator @animator
StateListAnimator @animator @stateListAnimator
颜色 int @color @color
ColorStateList @color @colorStateList

事件处理

数据绑定允许您编写处理从视图分派的事件的表达式,例如 onClick() 方法。事件属性名称由监听器方法的名称决定,但有一些例外。例如,View.OnClickListener 有一个 onClick() 方法,因此此事件的属性是 android:onClick

点击事件有一些特殊的事件处理程序,它们需要 android:onClick 之外的属性来避免冲突。您可以使用以下属性来避免此类冲突

监听器设置器 属性
SearchView setOnSearchClickListener(View.OnClickListener) android:onSearchClick
ZoomControls setOnZoomInClickListener(View.OnClickListener) android:onZoomIn
ZoomControls setOnZoomOutClickListener(View.OnClickListener) android:onZoomOut

您可以使用以下两种机制来处理事件,详细描述如下节所示

  • 方法引用:在您的表达式中,您可以引用符合监听器方法签名的方法。当表达式评估为方法引用时,数据绑定会将方法引用和所有者对象包装在监听器中,并将该监听器设置在目标视图上。如果表达式评估为 null,数据绑定不会创建监听器,而是设置一个 null 监听器。
  • 监听器绑定:这些是事件发生时评估的 lambda 表达式。数据绑定始终会创建一个监听器,并将其设置在视图上。当事件分派时,监听器会评估 lambda 表达式。

方法引用

您可以直接将事件绑定到处理方法,类似于将 android:onClick 分配给 Activity 中的方法。与 ViewonClick 属性相比,一个优点是表达式在编译时处理。因此,如果方法不存在或其签名不正确,您会收到编译时错误。

方法引用和监听器绑定的主要区别在于,实际的监听器实现是在数据绑定时创建的,而不是在事件触发时创建的。如果您更喜欢在事件发生时评估表达式,请使用监听器绑定

要将事件分配给其处理程序,请使用普通绑定表达式,其值为要调用的方法名称。例如,考虑以下示例布局数据对象

Kotlin

class MyHandlers {
    fun onClickFriend(view: View) { ... }
}

Java

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

绑定表达式可以将视图的点击监听器分配给 onClickFriend() 方法,如下所示

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

监听器绑定

监听器绑定是事件发生时运行的绑定表达式。它们类似于方法引用,但允许您运行任意数据绑定表达式。此功能适用于 Gradle 2.0 及更高版本的 Android Gradle 插件。

在方法引用中,方法的参数必须与事件监听器的参数匹配。在监听器绑定中,只有您的返回值必须与监听器的预期返回值匹配,除非它期望 void。例如,考虑以下具有 onSaveClick() 方法的 presenter 类

Kotlin

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

Java

public class Presenter {
    public void onSaveClick(Task task){}
}

您可以按如下方式将点击事件绑定到 onSaveClick() 方法

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

当表达式中使用回调时,数据绑定会自动创建必要的监听器并注册它以处理事件。当视图触发事件时,数据绑定会评估给定的表达式。与常规绑定表达式一样,在评估这些监听器表达式时,您会获得数据绑定的 null 安全性和线程安全性。

在前面的示例中,传递给 onClick(View)view 参数未定义。监听器绑定为监听器参数提供了两种选择:您可以忽略方法的所有参数,或为所有参数命名。如果您更喜欢为参数命名,则可以在表达式中使用它们。例如,您可以按如下方式编写前面的表达式

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

如果您想在表达式中使用参数,可以按如下方式操作

Kotlin

class Presenter {
    fun onSaveClick(view: View, task: Task){}
}

Java

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

您可以使用具有多个参数的 lambda 表达式

Kotlin

class Presenter {
    fun onCompletedChanged(task: Task, completed: Boolean){}
}

Java

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)}" />

如果您正在监听的事件返回的值类型不是 void,则您的表达式也必须返回相同类型的值。例如,如果您想监听长按(long click)事件,您的表达式必须返回一个布尔值。

Kotlin

class Presenter {
    fun onLongClick(view: View, task: Task): Boolean { }
}

Java

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

如果由于 null 对象而无法评估表达式,数据绑定会返回该类型的默认值,例如引用类型的 nullint 类型的 0,或 boolean 类型的 false

如果需要使用带有谓词(例如三元运算符)的表达式,可以使用 void 作为符号

android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"

避免复杂的监听器

监听器表达式功能强大,可以使您的代码更易于阅读。另一方面,包含复杂表达式的监听器会使您的布局更难阅读和维护。请保持您的表达式简单,只需将可用数据从 UI 传递给回调方法。将所有业务逻辑实现在您从监听器表达式调用的回调方法中。

导入、变量和包含

数据绑定库提供了导入、变量和包含等功能。导入功能使您可以在布局文件中轻松引用类。变量允许您描述可在绑定表达式中使用的属性。包含功能允许您在应用中重用复杂的布局。

导入

导入允许您在布局文件中引用类,就像在托管代码中一样。您可以在 data 元素内部使用零个或多个 import 元素。以下代码示例将 View 类导入到布局文件

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

导入 View 类允许您从绑定表达式中引用它。以下示例显示了如何引用 View 类的 VISIBLEGONE 常量

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

类型别名

当存在类名冲突时,您可以将其中一个类重命名为别名。以下示例将 com.example.real.estate 包中的 View 类重命名为 Vista

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

然后,您可以使用 Vista 引用 com.example.real.estate.View,并使用 View 引用布局文件中的 android.view.View

导入其他类

您可以将导入类型用作变量和表达式中的类型引用。以下示例显示了 UserList 用作变量的类型

<data>
    <import type="com.example.User"/>
    <import type="java.util.List"/>
    <variable name="user" type="User"/>
    <variable name="userList" type="List&lt;User>"/>
</data>

您可以使用导入类型来强制转换表达式的一部分。以下示例将 connection 属性强制转换为 User 类型

<TextView
   android:text="@{((User)(user.connection)).lastName}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

在表达式中引用静态字段和方法时,您也可以使用导入类型。以下代码导入 MyStringUtils 类并引用其 capitalize 方法

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

与托管代码一样,java.lang.* 会自动导入。

变量

您可以在 data 元素内部使用多个 variable 元素。每个 variable 元素描述了一个属性,该属性可以在布局上设置,以便在布局文件内的绑定表达式中使用。以下示例声明了 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>

变量类型在编译时进行检查,因此如果变量实现了 Observable 或是一个可观察集合,则必须在类型中反映出来。如果变量是未实现 Observable 接口的基类或接口,则这些变量不会被观察到。

当存在针对各种配置(例如横向或纵向)的不同布局文件时,变量会合并。这些布局文件之间不得有冲突的变量定义。

生成的绑定类为每个描述的变量都提供了 setter 和 getter。在调用 setter 之前,变量会采用默认的托管代码值——引用类型为 nullint 类型为 0boolean 类型为 false 等。

会生成一个名为 context 的特殊变量,以根据需要在绑定表达式中使用。 context 的值是来自根视图 getContext() 方法的 Context 对象。context 变量会被同名显式变量声明覆盖。

包含

您可以通过在属性中使用应用命名空间和变量名,将变量从包含布局传递到包含布局的绑定中。以下示例显示了来自 name.xmlcontact.xml 布局文件中包含的 user 变量

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

数据绑定不支持将 include 作为 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>

其他资源

要了解有关数据绑定的更多信息,请查阅以下其他资源。

示例

Codelabs

博文