Android Jetpack中DataBinding数据绑定布局和绑定表达式(三)
- 布局和绑定表达式
- 数据对象
- 绑定数据
- 表达式语言
- 缺少的运算
- Null 合并运算符
- 属性引用
- 避免出现 Null 指针异常
- 视图引用
- 集合
- 字符串字面量
- 资源
- 事件处理
- 方法引用
- 避免使用复杂的监听器
- 导入、变量和包含
- 导入
- 类型别名
- 导入其他类
- 变量
- 包含
布局和绑定表达式
借助表达式语言,您可以编写表达式来处理视图分派的事件。数据绑定库会自动生成将布局中的视图与您的数据对象绑定所需的类。
数据绑定布局文件略有不同,以根标记 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}" />
注意:布局表达式应保持精简,因为它们无法进行单元测试,并且拥有的 IDE 支持也有限。为了简化布局表达式,可以使用自定义绑定适配器。
数据对象
现在我们假设您有一个 plain-old 对象来描述 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());
如果您要在 Fragment、ListView 或 RecyclerView 适配器中使用数据绑定项,您可能更愿意使用绑定类或 DataBindingUtil 类的 inflate() 方法,如以下代码示例所示:
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,则 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}"
注意:绑定类将 ID 转换为驼峰式大小写。
在以下示例中,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]}"
注意:要使 XML 不含语法错误,您必须转义 < 字符。例如:不要写成 List 形式,而是必须写成 List<String>。
您还可以使用 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 表达式进行求值。
方法引用
事件可以直接绑定到处理脚本方法,类似于为 Activity 中的方法指定 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>
注意:表达式中的方法签名必须与监听器对象中的方法签名完全一致。
监听器绑定
监听器绑定是在事件发生时运行的绑定表达式。它们类似于方法引用,但允许您运行任意数据绑定表达式。此功能适用于 Gradle 2.0 版及更高版本的 Android Gradle 插件。
在方法引用中,方法的参数必须与事件监听器的参数匹配。在监听器绑定中,只有您的返回值必须与监听器的预期返回值相匹配(预期返回值无效除外)。例如,请参考以下具有 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){}
}
Xml使用
<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) { }
}
Xml使用
android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"
如果由于 null 对象而无法对表达式求值,则数据绑定将返回该类型的默认值。例如,引用类型返回 null,int 返回 0,boolean 返回 false,等等。
如果您需要将表达式与谓词(例如,三元运算符)结合使用,则可以使用 void 作为符号。
xml
android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"
避免使用复杂的监听器
监听器表达式功能非常强大,可以使您的代码非常易于阅读。另一方面,包含复杂表达式的监听器会使您的布局难以阅读和维护。这些表达式应该像将可用数据从界面传递到回调方法一样简单。您应该在从监听器表达式调用的回调方法中实现任何业务逻辑。
导入、变量和包含
数据绑定库提供了诸如导入、变量和包含等功能。通过导入功能,您可以轻松地在布局文件中引用类。通过变量功能,您可以描述可在绑定表达式中使用的属性。通过包含功能,您可以在整个应用中重复使用复杂的布局。
导入
通过导入功能,您可以轻松地在布局文件中引用类,就像在托管代码中一样。您可以在 data 元素使用多个 import 元素,也可以不使用。以下代码示例将 View 类导入到布局文件中:
Xml
<data>
<import type="android.view.View"/>
</data>
导入 View 类可让您通过绑定表达式引用该类。以下示例展示了如何引用 View 类的 VISIBLE 和 GONE 常量:
Xml
<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>
注意:Android Studio 尚不处理导入,因此导入变量的自动填充功能可能无法在您的 IDE 中使用。您的应用仍可以编译,并且您可以通过在变量定义中使用完全限定名称来解决这个 IDE 问题。
您还可以使用导入的类型来对表达式的一部分进行类型转换。以下示例将 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 的值是根视图的 getContext() 方法中的 Context 对象。context 变量会被具有该名称的显式变量声明替换。
包含
通过使用应用命名空间和特性中的变量名称,变量可以从包含的布局传递到被包含布局的绑定。以下示例展示了来自 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>