布局和绑定表达式

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

数据绑定布局文件略有不同,以 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()

绑定数据

每个布局文件都会生成一个绑定类。默认情况下,类的名称基于布局文件的名称,转换为 PascalCase,并添加了 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 中显示**测试**用户。或者,您可以使用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
color 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分配给方法的方式。与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>

侦听器绑定

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

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

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>

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

在前面的示例中,传递给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的值,则您的表达式也必须返回相同类型的值。例如,如果您想侦听触摸并按住(长按)事件,则您的表达式必须返回一个布尔值。

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对象而无法评估表达式,则数据绑定将返回该类型的默认值,例如引用类型的nullint0booleanfalse

如果您需要使用带有谓词的表达式(例如三元表达式),则可以使用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 之前,变量将采用默认的托管代码值 - 引用类型为 nullint0booleanfalse,等等。

为在绑定表达式中使用而生成一个名为 context 的特殊变量。context 的值为根视图的 Context 对象的 getContext() 方法。context 变量会被具有相同名称的显式变量声明覆盖。

包含

您可以通过在包含布局的属性中使用 app 命名空间和变量名,将变量传递到包含的布局的绑定中。以下示例显示了包含的 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>

数据绑定不支持将包含项作为合并元素的直接子元素。例如,以下布局不支持

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

博客文章