布局和绑定表达式

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

数据绑定布局文件略有不同,以根标记 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);
}

在运行时,应用会在界面中显示 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);

表达式语言

常见功能

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

  • 数学:+ - / * %
  • 字符串串联:+
  • 逻辑:&& ||
  • 二进制文件:& | ^
  • 一元组:+ - ! ~
  • Shift:>> >>> <<
  • 比较:== > < >= <=< 需要转义为 &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,则 null 合并运算符 (??) 会选择左侧运算数;如果左侧运算数为 null,则选择右侧运算数:

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

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

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

属性引用

表达式可以使用以下格式引用类中的属性,字段、getter 和 ObservableField 对象都相同:

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

避免 null 指针异常

生成的数据绑定代码会自动检查 null 值并避免 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 以外的属性来避免冲突。您可以使用以下属性来避免这些类型的冲突:

监听器 setter 属性
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 中的方法类似。与 View onClick 属性相比,一个优点是表达式在编译时得到处理。因此,如果该方法不存在或其签名不正确,您会收到编译时错误。

方法引用和监听器绑定之间的主要区别在于,实际的监听器实现是在绑定数据时创建的,而不是在事件触发时创建的。如果您希望在事件发生时对表达式求值,请使用监听器绑定

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

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 Plugin for Gradle 2.0 及更高版本中提供。

在方法引用中,方法的参数必须与事件监听器的参数匹配。在监听器绑定中,只有返回值必须与监听器的预期返回值一致,除非预期返回值为 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 的值,则您的表达式也必须返回相同类型的值。例如,如果您要监听轻触并按住(长按)事件,表达式必须返回一个布尔值。

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 对象而无法求值,则数据绑定会返回该类型的默认值,例如:对于引用类型,返回 null、对于 int0,或针对 boolean 返回 false

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

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

避免使用复杂的监听器

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

导入、变量和包含

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

导入

借助导入功能,您可以在布局文件内(例如在托管代码中)引用类。您可以在 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 之前,这些变量会采用默认的托管代码值 - null 用于引用类型,0 用于 intfalse 用于 boolean,等等。

系统会根据需要生成名为 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>

其他资源

如需详细了解数据绑定,请参阅下面列出的其他资源。

示例

Codelab

博文