DataBinding使用指南(一):布局和binding表达式
DataBinding使用指南(四):BindingAdapter
DataBinding使用指南(五):绑定布局视图到架构组件
文章目录
DataBinding库
DataBinding库是一个支持库,它允许开发者在 APP 中使用声明性格式而不是编程方式将数据源绑定到布局的UI组件中。
通常我们将布局定义在Activity中,然后使用UI框架方法在Java代码中调用 。例如,在下面的代码中调用findViewById()
查找一个TextView
控件并将其绑定到 viewModel
变量的userName
属性上:
TextView textView = findViewById(R.id.sample_text);
textView.setText(viewModel.getUserName());
下面将示例如何使用数据绑定库直接将文本指派到布局文件的控件上,这样我们就无需在Java代码中显式调用。请注意 @{}
在赋值表达式中的用法:
<TextView
android:text="@{viewmodel.userName}" />
使用DataBinding库,可以让我们在 Activity中删除很多UI框架的调用代码,使代码更简单,更易于维护,还可以提高Android应用性能,并且有助于防止内存泄漏和空指针异常。
下面我们将学习如何在Android应用中使用DataBinding库。
DataBinding依赖
首先要让我们的开发环境支持使用DataBinding库,包括支持Android Studio中的数据绑定代码。
数据绑定库提供了灵活性和广泛的兼容性 - 因为它是一个支持库,因此可以将其运行在Android 4.0(API级别14)或更高版本的设备上。
建议在项目中使用最新的 AndroidGradle 插件,版本在1.5.0 以上都支持数据绑定。
构建环境
首先请将dataBinding
元素添加到Android项目**模块(Module)**下的 build.gradle
文件中,如以下示例所示:
android {
...
dataBinding {
enabled = true
}
}
注意:如果一个应用模块依赖了使用数据绑定的库,也要配置数据绑定,即使该应用模块不直接使用数据绑定。
Android Studio支持数据绑定
Android Studio支持数据绑定代码的许多编辑特性。例如,它支持数据绑定表达式的以下功能:
- 语法高亮显示
- 表达式语言语法错误的标记
- XML代码完成
- 参考资料,包括 导航 (如导航到声明)和 快速文档
警告:数组和泛型,如 Observable类,可能会误报显示错误。
Layout Editor中的**Preview **窗格会显示数据绑定表达式的默认值(如果提供)。例如,将my_default
值显示到TextView
控件上:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName, default=my_default}"/>
如果你只需要在项目的设计阶段显示默认值,则可以使用 tools
属性而不是默认表达式值,更多请查看 Tools Attributes Reference 。
布局和绑定表达式
表达式语言允许你编写由 View 处理事件分发的表达式,数据绑定库会将布局中的View与数据对象绑定的类自动生成。
数据绑定布局文件略有不同,它以 layout
标签作为开头,后跟一个 data
元素和一个View
根元素,这个View
根元素就是非绑定布局文件中写的那种布局。以下是一个简单的数据绑定布局 activity_main.xml
:
<?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" />
布局中的表达式使用了@{}
语法将user
变量的属性写入到android:text
中,TextView
的文本被设置为user
变量的firstName
属性;
注意:布局表达式应尽量保持短小简单,因为它们没法进行单元测试,IDE的支持也很有限,为了简化布局表达式,你可以自定义
binding adapters
(后面会讲)。
数据对象
假设我们现在有一个描述 User 实体的普通对象(注意:这个User 类后面会经常出现):
public class User {
public final String firstName;
public final String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
这种类型的对象具有无法改变的数据,它通常在应用中被读取一次数据后再也不会改变。也可以使用遵循一系列约定的对象,例如使用访问器方法(就是getter方法),如下所示:
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;
}
}
从数据绑定的角度来看,上面的两个类是等价的。表达式@{user.firstName}
被用于 android:text
属性访问前一个类的firstName
字段和后一个类中的getFirstName()
方法,另外,它也可以用来解析firstName()
方法(如果这个方法存在的话)。
绑定数据
每个数据绑定布局文件都会生成一个绑定类,默认情况下,这个绑定类的名称是基于布局文件名称的,将布局文件名称转换为Pascal拼写法并向其添加 Binding 后缀(帕斯卡拼写法:将描述变量作用的所有单词的首字母大写,然后直接连接起来,单词之间没有连接符)。上面的那个布局文件名称是activity_main.xml
,所以相应的生成类是MainActivityBinding
。这个类包含布局属性(例如,user
变量)到布局View的所有绑定,并知道如何为绑定表达式赋值。推荐在 **inflating ** 布局时创建绑定方法,如下所示:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
User user = new User("Test", "User");
binding.setUser(user);
}
当APP运行时,会在UI中显示Test用户。或者,也可以使用LayoutInflater
获取View,如下例所示:
MainActivityBinding binding = MainActivityBinding.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
- 显式泛型调用
<T>
空合并操作符(Null coalescing operator)
空合并运算符(??)
:如果操作符的左操作数不为null
就选择左操作数否则选择右操作数:
android:text="@{user.displayName ?? user.lastName}"
这在功能上等同于:
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
属性引用
表达式可以通过使用以下格式来引用类中的属性,对于 fields, getters,和ObservableField对象,格式是相同的:
android:text="@{user.lastName}"
避免空指针异常
生成的数据绑定代码会自动检查null
值并避免空指针异常。例如,在表达式@{user.name}
中,如果user
是空,user.name
将默认为null
。如果你引用了int
类型的user.age
,那么数据绑定使用默认值0
;
集合
为方便起见,可以使用[]
运算符访问常见集合,例如数组,list,sparse list 和 map。
<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]}"
字符串文本
你可以使用单引号将属性值包围起来,这样就可以在表达式中使用双引号,如下例所示:
android:text='@{map["firstName"]}'
也可以使用双引号来包围属性值。这样做时,字符串文本应该用后引号 ` 括起来:
android:text="@{map[`firstName`]}"
资源 Resources
你可以使用以下语法在表达式中访问资源:
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
格式化字符串和复数可以通过提供参数来估算:
android:text="@{@string/nameFormat(firstName, lastName)}"
android:text="@{@plurals/banana(bananaCount)}"
当一个复数需要多个参数时,应该传递所有参数:
Have an orange
Have %d oranges
android:text="@{@plurals/orange(orangeCount, orangeCount)}"
有些资源需要显式类型评估,如下表所示:
Type | Normal reference | Expression reference |
---|---|---|
String[] | @array | @stringArray |
int[] | @array | @intArray |
TypedArray | @array | @typedArray |
Animator | @animator | @animator |
StateListAnimator | @animator | @stateListAnimator |
color int | @color | @color |
ColorStateList | @color | @colorStateList |
事件处理
数据绑定还可以编写处理View分发事件(例如,onClick()
方法)的表达式。除少数例外,事件属性名称由监听器(listener)方法的名称确定,例如,View.OnClickListener
有个方法onClick()
,所以这个事件的属性是android:onClick
。
为了避免冲突,处理某些针对点击事件的特殊事件,需要android:onClick
以外的其他属性,你可以使用以下属性来避免这些类型的冲突:
Class | Listener setter | Attribute |
---|---|---|
SearchView | setOnSearchClickListener(View.OnClickListener) | android:onSearchClick |
ZoomControls | setOnZoomInClickListener(View.OnClickListener) | android:onZoomIn |
ZoomControls | setOnZoomOutClickListener(View.OnClickListener) | android:onZoomOut |
你可以使用以下机制来处理事件:
- 方法引用:在表达式中你可以引用符合监听器方法签名的方法。当表达式评估为方法引用时,数据绑定将方法引用和所有者对象包装在监听器中,并将该监听器设置在目标 View 上。如果表达式评估为
null
,数据绑定不会创建监听器,而是设置一个null
监听器代替。 - 监听器绑定:当事件发生时lambda表达式将被 evaluate。数据绑定总是创建一个监听器,它在 View 上设置,当事件被分发时,监听器 evaluate lambda表达式。
方法引用
事件可以直接被绑定到处理方法上,类似于将android:onClick
分配给 activity 中的方法。与 View 的 onClick
属性相比,主要优势是表达式在编译时就会处理,所以如果该方法不存在或其签名不正确,则会收到编译时错误。
方法引用和监听器绑定之间的主要区别在于实际的监听器实现是在数据绑定时创建的,而不是在事件触发时创建的。如果你希望在事件发生时 evaluate 表达式,则应使用监听器绑定。
要将一个事件指定给其处理程序,请使用正常的绑定表达式,其值是要调用的方法名称。例如,请思考以下示例数据对象:
public class MyHandlers {
public void onClickFriend(View view) { ... }
}
这个绑定表达式可以将click监听器指定给给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插件。
在方法引用中,方法的参数必须与事件监听器的参数匹配。但在监听器绑定中,你的返回值必须与监听器的期望返回值相匹配(除非它预期为 void)。例如,下面代码中具有onSaveClick()
方法的Presenter
类:
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>
当在表达式中使用 callback 时,数据绑定会自动创建必要的监听器并将其注册为事件,当 View 触发事件时,数据绑定将评估给定的表达式,与常规绑定表达式中一样,在评估这些监听器表达式时,您仍然可以获得 null 和线程安全的数据绑定。
在上面的例子中,我们没有定义传递给 onClick(View)
方法的view
参数,监听器绑定为监听器参数提供了两种选择:你可以忽略该方法的所有参数或命名所有参数,如果你更喜欢命名参数,则可以在表达式中使用它们。例如,上面的表达式可以写成如下形式:
android:onClick="@{(view) -> presenter.onSaveClick(task)}"
或者,如果你想在表达式中使用参数,它可以如下:
public class Presenter {
public void onSaveClick(View view, Task task){}
}
android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}"
你还可以使用带有多个参数的 lambda 表达式:
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)}" />
如果正在监听的事件返回的值不是无效的,则表达式必须返回相同类型的值。例如,如果你想监听长按事件,你的表达式应该返回 boolean
值:
public class Presenter {
public boolean onLongClick(View view, Task task){}
}
android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"
如果由于null
对象而无法评估表达式,则数据绑定将返回该类型的默认值。例如,引用类型对应null
,int
对应0
,boolean
对应false
等。
如果你需要在表达式中使用断言(例如,三元操作符?:
),可以使用 void
。
android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"
避免过于复杂的监听器
Listener表达式可以说非常强大,它可以让我们的代码非常容易阅读,另一方面,如果 Listener 中包含了复杂表达式会使我们的布局难以阅读和维护,这些表达式应该像从UI中传递可用数据到回调方法一样简单。你应该在从 Listener表达式调用的回调方法内实现任何业务逻辑。
Imports, variables, and includes
数据绑定库提供了诸如 Imports, variables, 以及 includes等功能,Imports 可以轻松引用类到你的布局文件中,Variables 允许你描述可用于绑定表达式的属性,Includes 可以让你在整个应用中重复使用复杂的布局。
Imports
Imports 可以让我们轻松地在布局文件中引用类,在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>
警告: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"/>
Variables
我们可以在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
或者是一个可观察的集合,那么它应该被反映在类型中。如果变量是基本类型(int
,float
等),或没有实现Observable
接口,则它不是一个可观察变量。
当各种配置(例如,横向或纵向)有不同的布局文件时,变量会被合并,因此在这些布局文件之间不得存在冲突的变量定义。
生成的绑定类对每个描述的变量都有一个 setter 和 getter,这些变量会有一个默认值(引用类型为null
,int
为0
,boolean
为false
等),直到 setter 被调用。
在绑定表达式中会根据需要生成一个名为context
的特殊变量,context
的值是根View的getContext()
方法返回的Context
对象, context
变量被具有该名称的显式变量声明所覆盖。
Includes
在使用了 app 命名空间的布局中,可以将变量传递到include
布局的绑定中,以下示例显示了将user
变量传递到name.xml
和contact.xml
布局文件中(前提是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>
本文最近更新日期: 2018年4月26日。
DataBinding使用指南(一):布局和binding表达式