视图绑定
通过视图绑定功能,您可以更轻松地编写可与视图交互的代码。在模块中启用视图绑定之后,系统会为该模块中的每个 XML 布局文件生成一个绑定类。绑定类的实例包含对在相应布局中具有 ID 的所有视图的直接引用。
在大多数情况下,视图绑定会替代 findViewById
。
设置说明
注意:视图绑定在 Android Studio 3.6 Canary 11 及更高版本中可用。
视图绑定功能可按模块启用。要在某个模块中启用视图绑定,请将 viewBinding
元素添加到其 build.gradle
文件中,如下例所示:
android {
...
viewBinding {
enabled = true
}
}
如果您希望在生成绑定类时忽略某个布局文件,请将 tools:viewBindingIgnore="true"
属性添加到相应布局文件的根视图中:
<LinearLayout
...
tools:viewBindingIgnore="true" >
...
</LinearLayout>
用法
为某个模块启用视图绑定功能后,系统会为该模块中包含的每个 XML 布局文件生成一个绑定类。每个绑定类均包含对根视图以及具有 ID 的所有视图的引用。系统会通过以下方式生成绑定类的名称:将 XML 文件的名称转换为驼峰式大小写,并在末尾添加“Binding”一词。
例如,假设某个布局文件的名称为 result_profile.xml
:
<LinearLayout ... >
<TextView android:id="@+id/name" />
<ImageView android:cropToPadding="true" />
<Button android:id="@+id/button"
android:background="@drawable/rounded_button" />
</LinearLayout>
所生成的绑定类的名称就为 ResultProfileBinding
。此类具有两个字段:一个是名为 name
的 TextView
,另一个是名为 button
的 Button
。该布局中的 ImageView
没有 ID,因此绑定类中不存在对它的引用。
每个绑定类还包含一个 getRoot()
方法,用于为相应布局文件的根视图提供直接引用。在此示例中,ResultProfileBinding
类中的 getRoot()
方法会返回 LinearLayout
根视图。
以下几个部分介绍了生成的绑定类在 Activity 和 Fragment 中的使用。
在 Activity 中使用视图绑定
如需设置绑定类的实例以供 Activity 使用,请在 Activity 的 onCreate() 方法中执行以下步骤:
- 调用生成的绑定类中包含的静态
inflate()
方法。此操作会创建该绑定类的实例以供 Activity 使用。 - 通过调用
getRoot()
方法或使用 Kotlin 属性语法获取对根视图的引用。 - 将根视图传递到 setContentView(),使其成为屏幕上的活动视图。
private ResultProfileBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding = ResultProfileBinding.inflate(getLayoutInflater()); View view = binding.getRoot(); setContentView(view); }
您现在即可使用该绑定类的实例来引用任何视图:
binding.getName().setText(viewModel.getName()); binding.button.setOnClickListener(new View.OnClickListener() { viewModel.userClicked() });
在 Fragment 中使用视图绑定
如需设置绑定类的实例以供 Fragment 使用,请在 Fragment 的 onCreateView() 方法中执行以下步骤:
- 调用生成的绑定类中包含的静态
inflate()
方法。此操作会创建该绑定类的实例以供 Fragment 使用。 - 通过调用
getRoot()
方法或使用 Kotlin 属性语法获取对根视图的引用。 - 从
onCreateView()
方法返回根视图,使其成为屏幕上的活动视图。
注意:inflate()
方法会要求您传入布局膨胀器。如果布局已膨胀,您可以调用绑定类的静态 bind()
方法。如需了解详情,请查看视图绑定 GitHub 示例中的例子。
private ResultProfileBinding binding; @Override public View onCreateView (LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = ResultProfileBinding.inflate(inflater, container, false); View view = binding.getRoot(); return view; } @Override public void onDestroyView() { super.onDestroyView(); binding = null; }
您现在即可使用该绑定类的实例来引用任何视图:
binding.getName().setText(viewModel.getName()); binding.button.setOnClickListener(new View.OnClickListener() { viewModel.userClicked() });
注意:Fragment 的存在时间比其视图长。请务必在 Fragment 的 onDestroyView() 方法中清除对绑定类实例的所有引用。
与 findViewById 的区别
与使用 findViewById
相比,视图绑定具有一些很显著的优点:
- Null 安全:由于视图绑定会创建对视图的直接引用,因此不存在因视图 ID 无效而引发 Null 指针异常的风险。此外,如果视图仅出现在布局的某些配置中,则绑定类中包含其引用的字段会使用
@Nullable
标记。 - 类型安全:每个绑定类中的字段均具有与它们在 XML 文件中引用的视图相匹配的类型。这意味着不存在发生类转换异常的风险。
这些差异意味着布局和代码之间的不兼容将会导致构建在编译时(而非运行时)失败。
与数据绑定的对比
视图绑定和数据绑定均会生成可用于直接引用视图的绑定类。但是,视图绑定旨在处理更简单的用例,与数据绑定相比,具有以下优势:
- 更快的编译速度:视图绑定不需要处理注释,因此编译时间更短。
- 易于使用:视图绑定不需要特别标记的 XML 布局文件,因此在应用中采用速度更快。在模块中启用视图绑定后,它会自动应用于该模块的所有布局。
反过来,与数据绑定相比,视图绑定也具有以下限制:
- 视图绑定不支持布局变量或布局表达式,因此不能用于直接在 XML 布局文件中声明动态界面内容。
- 视图绑定不支持双向数据绑定。
考虑到这些因素,在某些情况下,最好在项目中同时使用视图绑定和数据绑定。您可以在需要高级功能的布局中使用数据绑定,而在不需要高级功能的布局中使用视图绑定。
布局和绑定表达式
借助表达式语言,您可以编写表达式来处理视图分派的事件。数据绑定库会自动生成将布局中的视图与您的数据对象绑定所需的类。
数据绑定布局文件略有不同,以根标记 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
实体:
public class User { public final String firstName; public final String lastName; public User(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } }
此类型的对象拥有永不改变的数据。应用包含读取一次后不会再发生改变的数据是很常见的。也可以使用遵循一组约定的对象,例如 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
变量)到布局视图的所有绑定,并且知道如何为绑定表达式指定值。建议的绑定创建方法是在扩充布局时创建,如以下示例所示:
@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
获取视图,如以下示例所示:
ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());
如果您要在 Fragment
、ListView
或 RecyclerView
适配器中使用数据绑定项,您可能更愿意使用绑定类或 DataBindingUtil 类的 inflate() 方法,如以下代码示例所示:
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<String>
形式,而是必须写成 List<String>
。
您还可以使用 object.key
表示法在映射中引用值。例如,以上示例中的 @{map[key]}
可替换为 @{map.key}
。
字符串字面量
您可以使用单引号括住特性值,这样就可以在表达式中使用双引号,如以下示例所示:
android:text='@{map["firstName"]}'
也可以使用双引号括住特性值。如果这样做,则还应使用反单引号 `
将字符串字面量括起来:
android:text="@{map[`firstName`]}"
双向数据绑定
使用单向数据绑定时,您可以为特性设置值,并设置对该特性的变化作出反应的监听器:
<CheckBox android:id="@+id/rememberMeCheckBox" android:checked="@{viewmodel.rememberMe}" android:onCheckedChanged="@{viewmodel.rememberMeChanged}" />
双向数据绑定为此过程提供了一种快捷方式:
<CheckBox android:id="@+id/rememberMeCheckBox" android:checked="@={viewmodel.rememberMe}" />
@={}
表示法(其中重要的是包含“=”符号)可接收属性的数据更改并同时监听用户更新。
为了对后台数据的变化作出反应,您可以将您的布局变量设置为 Observable
(通常为 BaseObservable)的实现,并使用 @Bindable 注释,如以下代码段所示:
public class LoginViewModel extends BaseObservable { // private Model data = ... @Bindable public Boolean getRememberMe() { return data.rememberMe; } public void setRememberMe(Boolean value) { // Avoids infinite loops. if (data.rememberMe != value) { data.rememberMe = value; // React to the change. saveData(); // Notify observers of a new value. notifyPropertyChanged(BR.remember_me); } } }
由于可绑定属性的 getter 方法称为 getRememberMe()
,因此属性的相应 setter 方法会自动使用名称 setRememberMe()
。
有关 BaseObservable
和 @Bindable
用法的详细信息,请参阅使用可观察的数据对象。
使用自定义特性的双向数据绑定
该平台为最常见的双向特性和更改监听器提供了双向数据绑定实现,您可以将其用作应用的一部分。如果您希望结合使用双向数据绑定和自定义特性,则需要使用 @InverseBindingAdapter 和 @InverseBindingMethod 注释。
例如,如果要在名为 MyView
的自定义视图中对 "time"
特性启用双向数据绑定,请完成以下步骤:
-
使用
@BindingAdapter
,对用来设置初始值并在值更改时进行更新的方法进行注释:@BindingAdapter("time") public static void setTime(MyView view, Time newValue) { // Important to break potential infinite loops. if (view.time != newValue) { view.time = newValue; } }
-
使用
@InverseBindingAdapter
对从视图中读取值的方法进行注释:@InverseBindingAdapter("time") public static Time getTime(MyView view) { return view.getTime(); }
此时,数据绑定知道在数据发生更改时要执行的操作(调用使用 @BindingAdapter 注释的方法)以及当 view 视特性发生更改时要调用的内容(调用 InverseBindingListener)。但是,它不知道特性何时或如何更改。
为此,您需要在视图上设置监听器。这可以是与您的自定义视图相关联的自定义监听器,也可以是通用事件,例如失去焦点或文本更改。将 @BindingAdapter
注释添加到设置监听器(用来监听属性更改)的方法中:
@BindingAdapter("app:timeAttrChanged") public static void setListeners( MyView view, final InverseBindingListener attrChange) { // Set a listener for click, focus, touch, etc. }
该监听器包含一个 InverseBindingListener
参数。您可以使用 InverseBindingListener
告知数据绑定系统,特性已更改。然后,该系统可以开始调用使用 @InverseBindingAdapter
注释的方法,依此类推。
注意:每个双向绑定都会生成“合成事件特性”。该特性与基本特性具有相同的名称,但包含后缀 "AttrChanged"
。合成事件特性允许库创建使用 @BindingAdapter
注释的方法,以将事件监听器与相应的 View
实例相关联。
实际上,此监听器包含一些复杂逻辑,包括用于单向数据绑定的监听器。用于文本属性更改的适配器 TextViewBindingAdapter 就是一个例子。
转换器
如果绑定到 View 对象的变量需要设置格式、转换或更改后才能显示,则可以使用 Converter
对象。
以显示日期的 EditText
对象为例:
<EditText
android:id="@+id/birth_date"
android:text="@={Converter.dateToString(viewmodel.birthDate)}"
/>
viewmodel.birthDate
属性包含 Long
类型的值,因此需要使用转换器设置格式。
由于使用了双向表达式,因此还需要使用反向转换器,以告知库如何将用户提供的字符串转换回后备数据类型(在本例中为 Long
)。此过程是通过向其中一个转换器添加 @InverseMethod 注释并让此注释引用反向转换器来完成的。以下代码段显示了此配置的一个示例:
public class Converter { @InverseMethod("stringToDate") public static String dateToString(EditText view, long oldValue, long value) { // Converts long to String. } public static long stringToDate(EditText view, String oldValue, String value) { // Converts String to long. } }
使用双向数据绑定的无限循环
使用双向数据绑定时,请注意不要引入无限循环。当用户更改特性时,系统会调用使用 @InverseBindingAdapter
注释的方法,并且该值将分配给后备属性。继而调用使用 @BindingAdapter
注释的方法,从而触发对使用 @InverseBindingAdapter
注释的方法的另一个调用,依此类推。
因此,通过比较使用 @BindingAdapter
注释的方法中的新值和旧值,可以打破可能出现的无限循环。