表达式语言允许您编写处理视图分派的事件的表达式。数据绑定库自动生成绑定布局中视图与数据对象所需的类。
数据绑定布局文件略有不同,以 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());
如果您在 Fragment
、ListView
或 RecyclerView
适配器中使用数据绑定项,您可能更喜欢使用绑定类的 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);
表达式语言
常见功能
表达式语言看起来很像在托管代码中找到的表达式。您可以在表达式语言中使用以下运算符和关键字
- 数学:
+ - / * %
- 字符串连接:
+
- 逻辑:
&& ||
- 二进制:
& | ^
- 一元:
+ - ! ~
- 移位:
>> >>> <<
- 比较:
== > < >= <=
(<
需要转义为<
) 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}"
避免空指针异常
生成的databinding代码会自动检查null
值并避免空指针异常。例如,在表达式@{user.name}
中,如果user
为null,则user.name
将分配其默认值null
。如果引用user.age
,其中age的类型为int
,则databinding使用默认值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<String>"/>
<variable name="sparse" type="SparseArray<String>"/>
<variable name="map" type="Map<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 |
事件处理
databinding允许您编写处理从视图分派的事件的表达式,例如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 |
您可以使用以下两个机制(在后续部分中详细介绍)来处理事件
- 方法引用:在您的表达式中,您可以引用符合侦听器方法签名的方法。当表达式计算为方法引用时,databinding会将方法引用和所有者对象包装在侦听器中,并将该侦听器设置为目标视图。如果表达式计算为
null
,则databinding不会创建侦听器,而是设置null
侦听器。 - 侦听器绑定:这些是在事件发生时计算的lambda表达式。databinding始终创建一个侦听器,并将其设置为视图。当分派事件时,侦听器会计算lambda表达式。
方法引用
您可以将事件直接绑定到处理程序方法,类似于您可以将android:onClick
分配给活动中的方法的方式。与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>
侦听器绑定
侦听器绑定是在事件发生时运行的绑定表达式。它们类似于方法引用,但允许您运行任意databinding表达式。此功能适用于Gradle版本2.0及更高版本的Android Gradle插件。
在方法引用中,方法的参数必须与事件侦听器的参数匹配。在侦听器绑定中,只有您的返回值必须与侦听器的预期返回值匹配,除非它期望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>
当在表达式中使用回调时,databinding会自动创建必要的侦听器并将其注册到事件。当视图触发事件时,databinding会计算给定的表达式。与常规绑定表达式一样,在评估这些侦听器表达式时,您可以获得databinding的空值和线程安全。
在前面的示例中,传递给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
对象而无法计算表达式,则databinding将返回该类型的默认值,例如引用类型的null
、int
的0
或boolean
的false
。
如果您需要使用带有谓词的表达式(例如,三元表达式),则可以使用void
作为符号
android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"
避免复杂的侦听器
侦听器表达式功能强大,可以使您的代码更易于阅读。另一方面,包含复杂表达式的侦听器会使您的布局难以阅读和维护。保持表达式的简单性,例如将可用的UI数据传递到回调方法。在从侦听器表达式调用的回调方法中实现任何业务逻辑。
导入、变量和包含
databinding库提供了导入、变量和包含等功能。导入使您能够轻松地在布局文件中引用类。变量允许您描述可以在绑定表达式中使用的属性。包含允许您在整个应用程序中重用复杂的布局。
导入
导入允许您在布局文件中引用类,就像在托管代码中一样。您可以在data
元素内使用零个或多个import
元素。以下代码示例将View
类导入到布局文件中
<data>
<import type="android.view.View"/>
</data>
导入View
类允许您从绑定表达式中引用它。以下示例显示了如何引用View
类的VISIBLE
和GONE
常量
<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
。
导入其他类
您可以将导入的类型用作变量和表达式中的类型引用。以下示例显示了 User
和 List
用作变量的类型
<data>
<import type="com.example.User"/>
<import type="java.util.List"/>
<variable name="user" type="User"/>
<variable name="userList" type="List<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
元素描述一个可以在布局上设置的属性,用于在布局文件中的绑定表达式中使用。以下示例声明了 user
、image
和 note
变量
<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
,int
为 0
,boolean
为 false
,等等。
为在需要时在绑定表达式中使用而生成了一个名为 context
的特殊变量。 context
的值为根视图的 Context
对象的 getContext()
方法。 context
变量会被具有相同名称的显式变量声明覆盖。
包含
您可以通过在属性中使用 app 命名空间和变量名称,将变量传递到包含布局的绑定中。以下示例显示了从 name.xml
和 contact.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>