数据绑定库
概览
数据绑定库是一种支持库,借助该库,您可以使用声明性格式(而非程序化地)将布局中的界面组件绑定到应用中的数据源。
1. 开始使用
了解如何准备开发环境以使用数据绑定库,包括在 Android Studio 中支持数据绑定代码。
2. 布局和绑定表达式
借助表达式语言,您可以编写将变量关联到布局中的视图的表达式。数据绑定库会自动生成将布局中的视图与您的数据对象绑定所需的类。该库提供了可在布局中使用的导入、变量和包含等功能。
该库的这些功能可与您的现有布局无缝地共存。例如,可以在表达式中使用的绑定变量在 data 元素(界面布局根元素的同级)内定义。这两个元素都封装在__layout__ 标记中,如以下示例所示
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="viewmodel"
type="com.myapp.data.ViewModel" />
</data>
<ConstraintLayout... /> <!-- UI layout's root element -->
</layout>
3. 使用可观察的数据对象
数据绑定库提供了可让您轻松地观察数据更改情况的类和方法。您不必操心在底层数据源发生更改时刷新界面。您可以将变量或其属性设为可观察。借助该库,您可以将对象、字段或集合设为可观察。
4. 生成的绑定类
数据绑定库可以生成用于访问布局变量和视图的绑定类。此页面展示了如何使用和自定义所生成的绑定类。
5. 绑定适配器
每一个布局表达式都有一个对应的绑定适配器,要求必须进行框架调用来设置相应的属性或监听器。
例如,绑定适配器负责调用 setText() 方法来设置文本属性,或者调用 setOnClickListener() 方法向点击事件添加监听器。最常拥的绑定适配器(例如针对本页面的示例中使用的 android:text 属性)可供您在 android.databinding.adapters 软件包中使用。如需常用绑定适配器列表,请参阅适配器。您也可以按照以下示例所示创建自定义适配器:
@BindingAdapter("app:goneUnless")
fun goneUnless(view: View, visible: Boolean) {
view.visibility = if (visible) View.VISIBLE else View.GONE
}
将布局视图绑定到架构组件
Android 支持库包含架构组件,您可以使用这些组件设计稳健、可测试且易维护的应用。您可以将架构组件与数据绑定库一起使用,以进一步简化界面开发。
双向数据绑定
数据绑定库支持双向数据绑定。此类绑定使用的表示法支持以下操作:
- 接收对属性的数据更改
- 监听用户对此属性的更新
使用入门
在app的build.gradle文件中添加dataBinding元素
android {
···
dataBinding {
enabled = true
}
}
布局和绑定表达式
数据绑定布局文件略有不同,以根标记 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 class User(val firstName: String, val lastName: String)
绑定数据
系统会为每个布局文件生成一个绑定类。默认情况下,类名称基于布局文件的名称,它会转换为 Pascal 大小写形式(大驼峰命名方式)并__在末尾添加 Binding 后缀__。以上布局文件名为 activity_main.xml
,因此生成的对应类为 ActivityMainBinding
。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivityMainBinding = DataBindingUtil.setContentView(
this, R.layout.activity_main)
/*
val binding: ActivityMainBinding = ActivityMainBinding.inflate(getLayoutInflater())
*/
binding.user = User("Test", "User")
}
如果您要在 Fragment、ListView 或 RecyclerView 适配器中使用数据绑定项,您可能更愿意使用绑定类或 DataBindingUtil 类的 inflate() 方法,如以下代码示例所示
val listItemBinding = ListItemBinding.inflate(layoutInflater, viewGroup, false)
// or
val listItemBinding = 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 合并运算符 (??) 选择左边运算数,如果左边运算数为 ,则选择右边运算数。
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。
集合
为方便起见,可使用 []
运算符访问常见集合,例如数组、列表、稀疏列表和映射。
<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)}"
当一个复数带有多个参数时,应传递所有参数:
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()
方法)。事件特性名称由监听器方法的名称确定,但有一些例外情况
类 | 监听器 setter | 特性 |
---|---|---|
SearchView | setOnSearchClickListener(View.OnClickListener) | android:onSearchClick |
ZoomControls | setOnZoomInClickListener(View.OnClickListener) | android:onZoomIn |
ZoomControls | setOnZoomOutClickListener(View.OnClickListener) | android:onZoomOut |
ZoomControls | setOnClickListener(View.OnClickListener) | android:onClick |
方法引用
事件可以直接绑定到处理脚本方法,类似于为 Activity 中的方法指定android:onClick 的方式。
与 View onClick 特性相比,一个主要优点是表达式在编译时进行处理,因此,如果该方法不存在或其签名不正确,则会收到编译时错误。
方法引用和监听器绑定之间的主要区别在于:
实际监听器实现是在绑定数据时创建的,而不是在事件触发时创建的。
如果您希望在事件发生时对表达式求值,则应使用监听器绑定。
示例
class MyHandlers {
fun onClickFriend(view: View) { ... }
}
<?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 插件。
在方法引用中,方法的参数必须与事件监听器的参数匹配。
在监听器绑定中,只有您的返回值必须与监听器的预期返回值相匹配(预期返回值无效除外)。
例子
class Presenter {
fun onSaveClick(task: Task){}
}
<?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>
监听器绑定提供两个监听器参数选项:您可以忽略方法的所有参数,也可以命名所有参数。如果您想命名参数,则可以在表达式中使用这些参数。
例如,上面的表达式可以写成如下形式:
android:onClick="@{(view) -> presenter.onSaveClick(task)}"
如果想在表达式中使用参数,则采用如下形式:
class Presenter {
fun onSaveClick(view: View, task: Task){}
}
android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}"
可以在 lambda 表达式中使用多个参数:
class Presenter {
fun onCompletedChanged(task: Task, completed: Boolean){}
}
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onCheckedChanged="@{(cb, isChecked) -> presenter.completeChanged(task, isChecked)}" />
如果您监听的事件返回类型不是 void 的值,则您的表达式也必须返回相同类型的值。
例如,如果要监听长按事件,表达式应返回一个布尔值
class Presenter {
fun onLongClick(view: View, task: Task): Boolean { }
}
android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"
如果由于 null
对象而无法对表达式求值,则数据绑定将返回该类型的默认值。例如,引用类型返回 null
,int
返回 0
,boolean
返回 false
,等等。
如果您需要将表达式与谓词(例如,三元运算符)结合使用,则可以使用 void
作为符号。
android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"
避免使用复杂的监听器
监听器表达式功能非常强大,可以使您的代码非常易于阅读。另一方面,包含复杂表达式的监听器会使您的布局难以阅读和维护。
这些表达式应该像将可用数据从界面传递到回调方法一样简单。您应该在从监听器表达式调用的回调方法中实现任何业务逻辑。
导入、变量和包含
导入
通过导入功能,您可以轻松地在布局文件中引用类。
示例
<data>
<import type="android.view.View"/>
<!-- 类型别名:适用于类名出现冲突的情况-->
<import type="com.example.real.estate.View"
alias="Vista"/>
</data>
<TextView
android:text="@{user.lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
在表达式中引用静态字段和方法时,也可以使用导入的类型。以下代码会导入 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"/>
变量
通过变量功能,您可以描述可在绑定表达式中使用的属性。
您可以在 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>
使用可观察的数据对象
可观察性是指一个对象将其数据变化通知给其他对象的能力。通过数据绑定库,您可以让对象、字段或集合变为可观察。
任何普通对象都可用于数据绑定,但修改对象不会自动使界面更新。通过数据绑定,数据对象可在其数据发生更改时通知其他对象,即监听器。可观察类有三种不同类型:对象、字段__和__集合。
当其中一个可观察数据对象绑定到界面并且该数据对象的属性发生更改时,界面会自动更新。
字段
在创建实现 Observable 接口的类时要完成一些操作,但如果您的类只有少数几个属性,则这样操作的意义不大。在这种情况下,您可以使用通用 Observable 类和以下特定于基元的类,将字段设为可观察字段:
- ObservableBoolean
- ObservableByte
- ObservableChar
- ObservableShort
- ObservableInt
- ObservableLong
- ObservableFloat
- ObservableDouble
- ObservableParcelable
可观测字段是自成一体的可观测对象。原始版本在访问操作时避免了装箱和解装箱。要使用这种机制,在Java编程语言中创建一个公共的最终属性,或者在Kotlin中创建一个只读属性,如下例所示
class User {
val firstName = ObservableField<String>()
val lastName = ObservableField<String>()
val age = ObservableInt()
}
要访问字段值,请使用 set() 和 get() 访问器方法,如下所示:
user.firstName = "Google"
val age = user.age
注意:Android Studio 3.1 及更高版本允许用 LiveData 对象替换可观察字段,从而为您的应用提供额外的好处。有关详情,请参阅使用 LiveData 将数据变化通知给界面。
集合
某些应用使用动态结构来保存数据。可观察集合允许使用键访问这些结构。当键为引用类型(如 String
)时,ObservableArrayMap
类非常有用,如以下示例所示:
ObservableArrayMap<String, Any>().apply {
put("firstName", "Google")
put("lastName", "Inc.")
put("age", 17)
}
在布局文件中,也可以使用以字符串为键的Map,如下
<data>
<import type="android.databinding.ObservableMap"/>
<variable name="user" type="ObservableMap<String, Object>"/>
</data>
…
<TextView
android:text="@{user.lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:text="@{String.valueOf(1 + (Integer)user.age)}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
当键是整数的时候,ObservableArrayList就很方便:
ObservableArrayList<Any>().apply {
add("Google")
add("Inc.")
add(17)
}
在布局文件中,允许List使用角标进行索引
<data>
<import type="android.databinding.ObservableList"/>
<import type="com.example.my.app.Fields"/>
<variable name="user" type="ObservableList<Object>"/>
</data>
…
<TextView
android:text='@{user[Fields.LAST_NAME]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:text='@{String.valueOf(1 + (Integer)user[Fields.AGE])}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
对象
实现
Observable
接口的类允许注册监听器,以便它们接收有关可观察对象的属性更改的通知。
Observable 接口具有添加和移除监听器的机制,但何时发送通知则必须由您决定。
为便于开发,数据绑定库提供了用于实现监听器注册机制的 BaseObservable 类。实现 BaseObservable 的数据类负责在属性更改时发出通知。具体操作过程是向 getter 分配 Bindable 注释,然后在 setter 中调用 notifyPropertyChanged() 方法,如以下示例所示:
class User : BaseObservable() {
@get:Bindable
var firstName: String = ""
set(value) {
field = value
notifyPropertyChanged(BR.firstName)
}
@get:Bindable
var lastName: String = ""
set(value) {
field = value
notifyPropertyChanged(BR.lastName)
}
}
数据绑定在模块包中生成一个名为 BR 的类,该类包含用于数据绑定的资源的 ID。在编译期间,Bindable 注释会在 BR 类文件中生成一个条目。如果数据类的基类无法更改,则 Observable 接口可以使用 PropertyChangeRegistry 对象实现,以便有效地注册和通知监听器。
生成的绑定类
生成的绑定类将布局变量与布局中的视图关联起来。绑定类的名称和包可以自定义。所有生成的绑定类都是从 ViewDataBinding 类继承而来的。
创建绑定对象
在对布局进行扩充后,应尽快创建绑定对象,以确保视图层次结构在通过表达式与布局内的视图绑定之前不会被修改。
将对象绑定到布局的最常用方法是在绑定类上使用静态方法。使用绑定类的 inflate()
方法来扩充视图层次结构并将对象绑定到该层次结构,如以下示例所示
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: MyLayoutBinding = MyLayoutBinding.inflate(layoutInflater)
}
inflate() 方法还有另外一个版本,这个版本不仅使用 LayoutInflater 对象,还使用 ViewGroup 对象,如以下示例所示:
val binding: MyLayoutBinding = MyLayoutBinding.inflate(getLayoutInflater(), viewGroup, false)
如果布局是使用其他机制扩充的,可单独绑定,如下所示:
val viewRoot = LayoutInflater.from(this).inflate(layoutId, parent, attachToParent)
val binding: ViewDataBinding? = DataBindingUtil.bind(viewRoot)
如果您要在 Fragment、ListView 或 RecyclerView 适配器中使用数据绑定项,您可能更愿意使用绑定类的 inflate() 方法或 DataBindingUtil 类,如以下代码示例所示:
val listItemBinding = ListItemBinding.inflate(layoutInflater, viewGroup, false)
// or
val listItemBinding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false)
带ID的视图
数据绑定库会针对布局中具有 ID 的每个视图在绑定类中创建不可变字段。例如,数据绑定库会根据以下布局创建 TextView
类型的 firstName
和 lastName
字段:
<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}"
android:id="@+id/firstName"/>
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.lastName}"
android:id="@+id/lastName"/>
</LinearLayout>
</layout>
该库一次性从视图层次结构中提取包含 ID 的视图。相较于针对布局中的每个视图调用 findViewById()
方法,这种机制速度更快。
变量
数据绑定库为布局中声明的每个变量生成访问器方法。例如,以下布局在绑定类中针对 user
、image
和 note
变量生成了 setter 和 getter 方法:
<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>
ViewStubs
与普通视图不同,
ViewStub
对象初始是一个不可见视图。当它们显示出来或者获得明确指示进行扩充时,它们会通过扩充另一个布局在布局中完成自我取代。由于
ViewStub
实际上会从视图层次结构中消失,因此绑定对象中的视图也必须消失,才能通过垃圾回收进行回收。由于视图是最终结果,因此ViewStubProxy
对象将取代生成的绑定类中的ViewStub
,让您能够访问ViewStub
(如果存在),同时还能访问ViewStub
进行扩充后的扩充版视图层次结构。在扩充其他布局时,必须为新布局建立绑定。因此,
ViewStubProxy
必须监听ViewStub
OnInflateListener
并在必要时建立绑定。由于在给定时间只能有一个监听器,因此ViewStubProxy
允许您设置OnInflateListener
,它将在建立绑定后调用这个监听器。
即时绑定
当可变或可观察对象发生更改时,绑定会按照计划在下一帧之前发生更改。但有时必须立即执行绑定。要强制执行,请使用
executePendingBindings()
方法。
高级绑定
动态变量
有时,系统并不知道特定的绑定类。例如,针对任意布局运行的 RecyclerView.Adapter
不知道特定绑定类。在调用 onBindViewHolder()
方法时,仍必须指定绑定值。
在以下示例中,RecyclerView
绑定到的所有布局都有 item
变量。BindingHolder
对象具有一个 getBinding()
方法,这个方法返回 ViewDataBinding
基类。
override fun onBindViewHolder(holder: BindingHolder, position: Int){
item: T = items.get(position)
holder.binding.setVariable(BR.item, item);
holder.binding.executePendingBindings();
}
注意:数据绑定库在模块包中生成一个名为
BR
的类,其中包含用于数据绑定的资源的 ID。在上例中,该库自动生成BR.item
变量。
后台线程
可以在后台线程中更改数据模型,但前提是这个模型不是集合。数据绑定会在求值过程中对每个变量/字段进行本地化,以避免出现并发问题。
自定义绑定类名称
默认情况下,绑定类是根据布局文件的名称生成的,以大写字母开头,移除下划线 ( _ ),将后一个字母大写,最后添加后缀 Binding。该类位于模块包下的 databinding
包中。例如,布局文件 contact_item.xml
会生成 ContactItemBinding
类。如果模块包是 com.example.my.app
,则绑定类放在 com.example.my.app.databinding
包中。
通过调整 data
元素的 class
特性,绑定类可重命名或放置在不同的包中。例如,以下布局在当前模块的 databinding
包中生成 ContactItem
绑定类:
<data class="ContactItem">
…
</data>
您可以在类名前添加句点和前缀,从而在其他文件包中生成绑定类。以下示例在模块包中生成绑定类:
<data class=".ContactItem">
…
</data>
您还可以使用完整软件包名称来生成绑定类。以下示例在 com.example
包中创建 ContactItem
绑定类:
<data class="com.example.ContactItem">
…
</data>
绑定适配器
绑定适配器负责发出相应的框架调用来设置值。例如,设置属性值就像调用
setText()
方法一样。再比如,设置事件监听器就像调用setOnClickListener()
方法。数据绑定库允许您通过使用适配器指定为设置值而调用的方法、提供您自己的绑定逻辑,以及指定返回对象的类型。
设置属性值
只要绑定值发生更改,生成的绑定类就必须使用绑定表达式在视图上调用 setter 方法。您可以允许数据绑定库自动确定方法、显式声明方法或提供选择方法的自定义逻辑。
自动选择方法
指定自定义方法名称
某些特性拥有名称不符的 setter。在这些情况下,某个特性可能会使用 BindingMethods
注释与 setter 相关联。
注释与类一起使用,可以包含多个 BindingMethod
注释,每个注释对应一个重命名的方法。绑定方法是可添加到应用中任何类的注释。
在以下示例中,android:tint
特性与 setImageTintList(ColorStateList)
方法相关联,而不与 setTint()
方法相关联:
@BindingMethods(value = [
BindingMethod(
type = android.widget.ImageView::class,
attribute = "android:tint",
method = "setImageTintList")])
提供自定义逻辑
__某些特性需要自定义绑定逻辑。__例如,android:paddingLeft
特性没有关联的 setter,而是提供了 setPadding(left, top, right, bottom)
方法。使用 BindingAdapter
注释的静态绑定适配器方法支持自定义特性 setter 的调用方式。
Android 框架类的特性已经创建了 BindingAdapter
注释。例如,以下示例展示了 paddingLeft
特性的绑定适配器:
@BindingAdapter("android:paddingLeft")
fun setPaddingLeft(view: View, padding: Int) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom())
}
第一个参数用于确定与特性关联的视图类型;第二个参数用于确定在给定特性的绑定表达式中接受的类型。
绑定适配器对其他类型的自定义很有用。例如,可以通过工作器线程调用自定义加载程序来加载图片。
出现冲突时,您定义的绑定适配器会替换由 Android 框架提供的默认适配器。
您还可以使用接收多个特性的适配器,如以下示例所示:
@BindingAdapter("imageUrl", "error")
fun loadImage(view: ImageView, url: String, error: Drawable) {
Picasso.get().load(url).error(error).into(view)
}
可以在布局中使用适配器,如以下示例所示。请注意,@drawable/venueError
引用应用中的资源。使用 @{}
将资源括起来可使其成为有效的绑定表达式。
<ImageView
app:imageUrl="@{venue.imageUrl}"
app:error="@{@drawable/venueError}" />
注意:数据绑定库在匹配时会忽略自定义命名空间。
如果 ImageView
对象同时使用了 imageUrl
和 error
,并且 imageUrl
是字符串,error
是 Drawable
,则会调用适配器。
如果您希望在设置了任意特性时调用适配器,则可以将适配器的可选 requireAll
标记设置为 false
,如以下示例所示:
@BindingAdapter(value = ["imageUrl", "placeholder"], requireAll = false)
fun setImageUrl(imageView: ImageView, url: String?, placeHolder: Drawable?) {
if (url == null) {
imageView.setImageDrawable(placeholder);
} else {
MyImageLoader.loadInto(imageView, url, placeholder);
}
}
注意:出现冲突时,绑定适配器会替换默认的数据绑定适配器。
绑定适配器方法可以选择性在处理程序中使用旧值。同时采用旧值和新值的方法应该先为特性声明所有旧值,然后再声明新值,如以下示例所示:
@BindingAdapter("android:paddingLeft")
fun setPaddingLeft(view: View, oldPadding: Int, newPadding: Int) {
if (oldPadding != newPadding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom())
}
}
事件处理程序只能与具有一种抽象方法的接口或抽象类一起使用,如以下示例所示:
@BindingAdapter("android:onLayoutChange")
fun setOnLayoutChangeListener(
view: View,
oldValue: View.OnLayoutChangeListener?,
newValue: View.OnLayoutChangeListener?
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
if (oldValue != null) {
view.removeOnLayoutChangeListener(oldValue)
}
if (newValue != null) {
view.addOnLayoutChangeListener(newValue)
}
}
}
按如下所示在布局中使用此事件处理脚本:
<View
android:onLayoutChange="@{() -> handler.layoutChanged()}"/>
__当监听器有多个方法时,必须将它拆分为多个监听器。__例如,View.OnAttachStateChangeListener
有两个方法:onViewAttachedToWindow(View)
和 onViewDetachedFromWindow(View)
。该库提供了两个接口,用于区分它们的特性和处理脚本:
// Translation from provided interfaces in Java:
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
interface OnViewDetachedFromWindow {
fun onViewDetachedFromWindow(v: View)
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
interface OnViewAttachedToWindow {
fun onViewAttachedToWindow(v: View)
}
因为更改一个监听器也会影响另一个监听器,所以需要适用于其中一个特性或同时适用于这两个特性的适配器。您可以在注释中将
requireAll
设置为false
,以指定并非必须为每个特性都分配绑定表达式,如以下示例所示:
@BindingAdapter(
"android:onViewDetachedFromWindow",
"android:onViewAttachedToWindow",
requireAll = false
)
fun setListener(view: View, detach: OnViewDetachedFromWindow?, attach: OnViewAttachedToWindow?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
val newListener: View.OnAttachStateChangeListener?
newListener = if (detach == null && attach == null) {
null
} else {
object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {
attach.onViewAttachedToWindow(v)
}
override fun onViewDetachedFromWindow(v: View) {
detach.onViewDetachedFromWindow(v)
}
}
}
val oldListener: View.OnAttachStateChangeListener? =
ListenerUtil.trackListener(view, newListener, R.id.onAttachStateChangeListener)
if (oldListener != null) {
view.removeOnAttachStateChangeListener(oldListener)
}
if (newListener != null) {
view.addOnAttachStateChangeListener(newListener)
}
}
}
以上示例比一般情况稍微复杂一些,因为
View
类使用addOnAttachStateChangeListener()
和removeOnAttachStateChangeListener()
方法,而非OnAttachStateChangeListener
的 setter 方法。android.databinding.adapters.ListenerUtil
类有助于跟踪以前的监听器,以便在绑定适配器中将它们移除。通过用
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
注释接口OnViewDetachedFromWindow
和OnViewAttachedToWindow
,数据绑定代码生成器知道只应在运行 Android 3.1(API 级别 12)及更高级别(addOnAttachStateChangeListener()
方法支持的相同版本)时生成监听器。
对象转换
自动对象转换
当绑定表达式返回 Object
时,库会选择用于设置属性值的方法。Object
会转换为所选方法的参数类型。对于使用 ObservableMap
类存储数据的应用,这种行为非常便捷,如以下示例所示:
<TextView
android:text='@{userMap["lastName"]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
注意:您还可以使用 object.key
表示法引用映射中的值。例如,以上示例中的 @{userMap["lastName"]}
可替换为 @{userMap.lastName}
。
表达式中的
userMap
对象会返回一个值,该值会自动转换为用于设置android:text
属性值的setText(CharSequence)
方法中的参数类型。如果参数类型不明确,则必须在表达式中强制转换返回类型。
自定义转换
在某些情况下,需要在特定类型之间进行自定义转换。
例如,视图的 android:background
特性需要 Drawable
,但指定的 color
值是整数。以下示例展示了某个特性需要 Drawable
,但结果提供了一个整数:
<View
android:background="@{isError ? @color/red : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
每当需要 Drawable
且返回整数时,int
都应转换为 ColorDrawable
。您可以使用带有 BindingConversion
注释的静态方法完成这个转换,如下所示
@BindingConversion
fun convertColorToDrawable(color: Int) = ColorDrawable(color)
但是,__绑定表达式中提供的值类型必须保持一致。__您不能在同一个表达式中使用不同的类型,如以下示例所示:
<View
android:background="@{isError ? @drawable/error : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
将布局视图绑定到架构组件
使用LiveData将数据变化通知给页面
与实现
Observable
的对象(例如可观察字段)不同,LiveData
对象了解订阅数据更改的观察器的生命周期。
要将 LiveData
对象与绑定类一起使用,需要指定生命周期所有者来定义 对象的范围。
以下示例在绑定类实例化后将 Activity 指定为生命周期所有者:
class ViewModelActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// Inflate view and obtain an instance of the binding class.
val binding: UserBinding = DataBindingUtil.setContentView(this, R.layout.user)
// Specify the current activity as the lifecycle owner.
binding.setLifecycleOwner(this)
}
}
您可以根据使用 ViewModel 管理界面相关数据中所述,使用 ViewModel 组件来将数据绑定到布局。在 ViewModel 组件中,您可以使用 LiveData 对象转换数据或合并多个数据源。
以下示例展示了如何在 ViewModel 中转换数据:
class ScheduleViewModel : ViewModel() {
val userName: LiveData
init {
val result = Repository.userName
userName = Transformations.map(result) { result -> result.value }
}
}
使用ViewModel管理界面相关数据
要将 ViewModel
组件与数据绑定库一起使用,必须实例化从 类继承而来的组件,获取绑定类的实例,并将您的 组件分配给绑定类中的属性。
以下示例展示了如何将组件与库结合使用:
class ViewModelActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// Obtain the ViewModel component.
UserModel userModel = ViewModelProviders.of(getActivity())
.get(UserModel.class)
// Inflate view and obtain an instance of the binding class.
val binding: UserBinding = DataBindingUtil.setContentView(this, R.layout.user)
// Assign the component to a property in the binding class.
binding.viewmodel = userModel
}
}
在布局中,使用绑定表达式将 ViewModel
组件的属性和方法分配给对应的视图,如以下示例所示:
<CheckBox
android:id="@+id/rememberMeCheckBox"
android:checked="@{viewmodel.rememberMe}"
android:onCheckedChanged="@{() -> viewmodel.rememberMeChanged()}" />
使用Observable ViewModel以更好地控制绑定适配器
使用实现
Observable
的ViewModel
组件可让您更好地控制应用中的绑定适配器。例如,这种模式可让您更多地控制数据更改时的通知,您还可以指定自定义方法来设置双向数据绑定中的属性值。
要实现可观察的 ViewModel
组件,您必须创建一个从 类继承而来并实现 Observable
接口的类。您可以使用 addOnPropertyChangedCallback()
和 removeOnPropertyChangedCallback()
方法提供观察器订阅或取消订阅通知时的自定义逻辑。您还可以在 notifyPropertyChanged()
方法中提供属性更改时运行的自定义逻辑。以下代码示例展示了如何实现一个可观察的 ViewModel
:
/**
* A ViewModel that is also an Observable,
* to be used with the Data Binding Library.
*/
open class ObservableViewModel : ViewModel(), Observable {
private val callbacks: PropertyChangeRegistry = PropertyChangeRegistry()
override fun addOnPropertyChangedCallback(
callback: Observable.OnPropertyChangedCallback) {
callbacks.add(callback)
}
override fun removeOnPropertyChangedCallback(
callback: Observable.OnPropertyChangedCallback) {
callbacks.remove(callback)
}
/**
* Notifies observers that all properties of this instance have changed.
*/
fun notifyChange() {
callbacks.notifyCallbacks(this, 0, null)
}
/**
* Notifies observers that a specific property has changed. The getter for the
* property that changes should be marked with the @Bindable annotation to
* generate a field in the BR class to be used as the fieldId parameter.
*
* @param fieldId The generated BR id for the Bindable field.
*/
fun notifyPropertyChanged(fieldId: Int) {
callbacks.notifyCallbacks(this, fieldId, null)
}
}
双向数据绑定
使用单向数据绑定,您可以为特性设置值,并设置对该特性更改作出反应的监听器
<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
注释,如以下代码段所示:
class LoginViewModel : BaseObservable {
// val data = ...
@Bindable
fun getRememberMe(): Boolean {
return data.rememberMe
}
fun setRememberMe(value: Boolean) {
// 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)
}
}
}
使用自定义特性的双向数据绑定
如果您希望结合使用双向数据绑定和自定义特性,则需要使用 @InverseBindingAdapter
和 @InverseBindingMethod
注释。
例如,如果要在名为 MyView
的自定义视图中对 "time"
特性启用双向数据绑定,请完成以下步骤:
-
使用
@BindingAdapter
,对用来设置初始值并在值更改时进行更新的方法进行注释:@BindingAdapter("time") @JvmStatic fun setTime(view: MyView, newValue: Time) { // Important to break potential infinite loops. if (view.time != newValue) { view.time = newValue } }
-
使用
@InverseBindingAdapter
对从视图中读取值的方法进行注释:@InverseBindingAdapter("time") @JvmStatic fun getTime(view: MyView) : Time { return view.getTime() }
此时,数据绑定知道在数据发生更改时要执行的操作(调用使用
@BindingAdapter
注释的方法)以及当 view 视特性发生更改时要调用的内容(调用InverseBindingListener
)。但是,它不知道特性何时或如何更改。为此,您需要在视图上设置监听器。这可以是与您的自定义视图相关联的自定义监听器,也可以是通用事件,例如失去焦点或文本更改。将
@BindingAdapter
注释添加到设置监听器(用来监听属性更改)的方法中:@BindingAdapter("app:timeAttrChanged") @JvmStatic fun setListeners( view: MyView, attrChange: InverseBindingListener ) { // Set a listener for click, focus, touch, etc. }
该监听器包含一个
InverseBindingListener
参数。您可以使用InverseBindingListener
告知数据绑定系统,特性已更改。然后,该系统可以开始调用使用@InverseBindingAdapter
注释的方法,依此类推。注意:每个双向绑定都会生成“合成事件特性”。该特性与基本特性具有相同的名称,但包含后缀
"AttrChanged"
。合成事件特性允许库创建使用@BindingAdapter
注释的方法,以将事件监听器与相应的View
实例相关联。
转换器
如果绑定到 View
对象的变量需要设置格式、转换或更改后才能显示,则可以使用 Converter
对象。
以显示日期的 EditText
对象为例:
<EditText
android:id="@+id/birth_date"
android:text="@={Converter.dateToString(viewmodel.birthDate)}"
/>
viewmodel.birthDate
属性包含Long
类型的值,因此需要使用转换器设置格式。
由于使用了双向表达式,因此还需要使用反向转换器,以告知库如何将用户提供的字符串转换回后备数据类型(在本例中为 Long
)。此过程是通过向其中一个转换器添加 @InverseMethod
注释并让此注释引用反向转换器来完成的。以下代码段显示了此配置的一个示例:
object Converter {
@InverseMethod("stringToDate")
fun dateToString(
view: EditText, oldValue: Long,
value: Long
): String {
// Converts long to String.
}
fun stringToDate(
view: EditText, oldValue: String,
value: String
): Long {
// Converts String to long.
}
}
使用双向数据绑定的无限循环
使用双向数据绑定时,请注意不要引入无限循环。当用户更改特性时,系统会调用使用
@InverseBindingAdapter
注释的方法,并且该值将分配给后备属性。继而调用使用@BindingAdapter
注释的方法,从而触发对使用@InverseBindingAdapter
注释的方法的另一个调用,依此类推。
因此,通过比较使用 @BindingAdapter
注释的方法中的新值和旧值,可以打破可能出现的无限循环
双向特性
当您使用下表中的特性时,该平台提供对双向数据绑定的内置支持。有关平台如何提供此类支持的详细信息,请参阅相应绑定适配器的实现:
类 | 特性 | 绑定适配器 |
---|---|---|
AdapterView |
android:selectedItemPosition android:selection |
AdapterViewBindingAdapter |
CalendarView |
android:date |
CalendarViewBindingAdapter |
CompoundButton |
android:checked |
CompoundButtonBindingAdapter |
DatePicker |
android:year android:month android:day |
DatePickerBindingAdapter |
NumberPicker |
android:value |
NumberPickerBindingAdapter |
RadioButton |
android:checkedButton |
RadioGroupBindingAdapter |
RatingBar |
android:rating |
RatingBarBindingAdapter |
SeekBar |
android:progress |
SeekBarBindingAdapter |
TabHost |
android:currentTab |
TabHostBindingAdapter |
TextView |
android:text |
TextViewBindingAdapter |
TimePicker |
android:hour android:minute |
TimePickerBindingAdapter |