Jetpack学习2—布局&表达式
概览
Data Binding Library
Data Binding Library是一个支持库,它允许您使用声明格式(而不是编程方式)将布局中的UI组件绑定到应用程序中的数据源。
布局通常在带有调用UI框架方法的代码的活动中定义。例如,下面的代码调用findViewById()
来查找TextView
小部件并将其绑定到viewModel
变量的userName
属性:
TextView textView = findViewById(R.id.sample_text);
textView.setText(viewModel.getUserName());
下面的示例展示如何使用数据绑定库在布局文件中直接为小部件分配文本。这样就不需要调用上面所示的任何Java代码。注意在赋值表达式中使用@{}语法:
<TextView
android:text="@{viewmodel.userName}" />
布局文件中的绑定组件允许您删除Activity中的许多UI框架调用,从而使它们更简单、更易于维护。这还可以提高应用程序的性能,帮助防止内存泄漏和空指针异常。
Using the Data Binding Library
使用以下页面学习如何在Android应用程序中使用数据绑定库。
了解如何使开发环境准备好使用数据绑定库,包括Android Studio中对数据绑定代码的支持。
表达式语言允许您编写将变量连接到布局中的视图的表达式。数据绑定库自动生成将布局中的视图与数据对象绑定所需的类。该库提供了导入、变量等特性,并包括可以在布局中使用的特性。
这个库的这些特性与您现有的布局无缝地共存。例如,表达式中可以使用的绑定变量定义在一个数据元素中,该数据元素是UI布局的根元素的兄弟元素。这两个元素都被包装在一个布局元素中,如下面的例子所示:
<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>
数据绑定库提供了类和方法来方便地观察数据的变化。当底层数据源发生更改时,您不必担心刷新UI。你可以让你的变量或它们的属性可见。该库允许您使对象、字段或集合可观察。
Data Binding Library生成用于访问布局的变量和视图的绑定类。这个页面向您展示了如何使用和定制生成的绑定类。
对于每个布局表达式,都有一个绑定适配器进行框架调用,以设置相应的属性或监听器。例如,绑定适配器可以调用setText()方法来设置文本属性,或者调用setOnClickListener()方法来向单击事件添加监听器。您可以在android.databinding中使用最常见的绑定适配器,例如本文示例中使用的android:text属性的适配器。适配器包。有关常见绑定适配器的列表,请参见adapters。您还可以创建自定义适配器,如下面的示例所示:
@BindingAdapter("app:goneUnless")
fun goneUnless(view: View, visible: Boolean) {
view.visibility = if (visible) View.VISIBLE else View.GONE
}
Android支持库包括架构组件,您可以使用它们来设计健壮、可测试和可维护的应用程序。您可以使用带有数据绑定库的体系结构组件来进一步简化UI的开发。
数据绑定库支持双向数据绑定。这种类型的绑定使用的符号支持接收属性的数据更改并同时监听该属性的用户更新。
额外的资源
开始
了解如何使开发环境准备好使用数据绑定库,包括Android Studio中对数据绑定代码的支持。
数据绑定库提供了灵活性和广泛的兼容性——它是一个支持库,所以您可以在运行Android 4.0 (API级别14)或更高的设备上使用它。
建议在你的项目中使用最新的Android插件Gradle。但是,1.5.0及更高版本支持数据绑定。有关更多信息,请参见如何更新Gradle的Android插件。
构建环境
要开始数据绑定,请从Android SDK管理器中的支持存储库中下载该库。有关更多信息,请参见更新IDE和SDK工具。
要将应用程序配置为使用数据绑定,请将dataBinding
元素添加app模块中的build.gradle
文件,如下图所示:
android {
...
dataBinding {
enabled = true
}
}
注意:必须为依赖使用数据绑定的库的应用程序模块配置数据绑定,即使应用程序模块不直接使用数据绑定。
Android Studio支持数据绑定
Android Studio支持许多数据绑定代码的编辑功能。例如,它支持以下数据绑定表达式的特性:
注意:数组和泛型类型(如Observable类)可能会不正确地显示错误。
布局编辑器中的预览窗格显示数据绑定表达式的默认值(如果提供的话)。例如,预览窗格会在下面的示例中声明的TextView小部件上显示my_default值:
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName, default=my_default}"/>
如果您只需要在项目的设计阶段显示默认值,则可以使用tools属性而不是默认表达式值,如Tools Attributes Reference。
用于绑定类的新数据绑定编译器
Android Gradle plugin version 3.1.0-alpha06包含一个新的数据绑定编译器,用于生成绑定类。新编译器递增地创建绑定类,这在大多数情况下加快了构建过程。有关绑定类的更多信息,请参见 Generated binding classes。
以前版本的数据绑定编译器在编译托管代码的相同步骤中生成绑定类。如果托管代码无法编译,您可能会收到多个错误报告,说明没有找到绑定类。新的数据绑定编译器通过在托管编译器构建应用程序之前生成绑定类来防止这些错误。
要启用新的数据绑定编译器,请在gradle.properties
中添加以下选项:
android.databinding.enableV2=true
您还可以在gradle命令中启用新的编译器,方法是添加以下参数:
-Pandroid.databinding.enableV2=true
注意:Android Plugin 3.1中的新数据绑定编译器不向后兼容。您需要生成具有此功能的所有绑定类,以便利用增量编译。然而,Android Plugin 3.2中的新编译器与以前版本生成的绑定类是兼容的。默认情况下,3.2版中的新编译器是启用的。
在启用新的数据绑定编译器时,将应用以下行为更改:
- Android Gradle插件在编译托管代码之前为你的布局生成绑定类。
- 如果一个布局包含在多个目标资源配置中,数据绑定库将使用android.view.View作为共享相同资源id但不共享视图类型的所有视图的默认视图类型。
- 库模块的绑定类被编译并打包到相应的Android Archive (AAR)文件中。依赖于这些库模块的应用程序模块不再需要重新生成绑定类。有关AAR文件的更多信息,请参见创建Android库。
- 模块的绑定适配器不再能够更改模块依赖项的适配器的行为。绑定适配器只影响其自身模块中的代码和模块的使用者。
布局和绑定表达式
表达式语言允许您编写处理视图分派的事件的表达式。数据绑定库自动生成将布局中的视图与数据对象绑定所需的类。
数据绑定布局文件略有不同,首先是布局的根标记,然后是数据元素和视图根元素。这个视图元素是您在非绑定布局文件中的根元素。下面的代码显示了一个示例布局文件:
<?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文本被设置为用户变量的firstName属性:
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}" />
注意:布局表达式应该保持小而简单,因为它们不能进行单元测试,并且IDE支持有限。为了简化布局表达式,可以使用自定义绑定适配器。
数据对象
现在让我们假设您有一个普通的对象来描述用户实体:
data class User(val firstName: String, val lastName: String)
这种类型的对象具有永不更改的数据。在应用程序中,很常见的情况是数据只读取一次,以后不会更改。也可以使用遵循一组约定的对象,例如Java中访问器方法的使用,如下面的示例所示:
// Not applicable in Kotlin.
data class User(val firstName: String, val lastName: String)
从数据绑定的角度来看,这两个类是等价的。@{user.firstName}
用于该android:text
属性的表达式访问firstName
前一类中的字段和getFirstName()
后一类中的 方法。或者,firstName()
如果存在该方法,也可以解决。
绑定数据
为每个布局文件生成一个绑定类。默认情况下,类的名称基于布局文件的名称,将其转换为Pascal大小写并向其添加Binding后缀。上面的布局文件名是activity_main.xml,因此相应生成的类是ActivityMainBinding。该类保存从布局属性(例如,用户变量)到布局视图的所有绑定,并且知道如何为绑定表达式赋值。创建绑定的推荐方法是在扩展布局时进行绑定,如下面的示例所示:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivityMainBinding = DataBindingUtil.setContentView(
this, R.layout.activity_main)
binding.user = User("Test", "User")
}
At runtime, the app displays the Test user in the UI. Alternatively, you can get the view using a LayoutInflater
, as shown in the following example:
val binding: ActivityMainBinding = ActivityMainBinding.inflate(getLayoutInflater())
如果您在Fragment、ListView或RecyclerView适配器中使用数据绑定项,您可能更喜欢使用bindings类或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
- 显式通用调用
空合并操作符
空合并操作符(??
)如果左操作数不为空,则选择左操作数;否则,则选择右操作数。
android:text="@{user.displayName ?? user.lastName}"
这在功能上等同于:
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
属性引用
表达式可以使用以下格式引用类中的属性,这与字段、getter和ObservableField
对象的格式相同:
android:text="@{user.lastName}"
避免空指针异常
生成的数据绑定代码会自动检查null
值并避免空指针异常。例如,在表达式中@{user.name}
,if user
为null,user.name
则赋值为默认值null
。如果您引用user.age
age,其中age是类型int
,那么数据绑定使用默认值0
。
集合
为了方便,可以使用[]操作符访问 arrays, lists, sparse lists, 和 maps等公共集合。
<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中的值。例如,@{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)}"
有些资源需要显式的类型引用,如下表所示
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 |
事件处理
数据绑定允许您编写从视图调度的表达式处理事件(例如,onClick()
方法)。事件属性名称由监听器方法的名称确定,但有一些例外。例如,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 |
您可以使用以下机制来处理事件:
- 方法引用:在表达式中,可以引用符合监听器方法签名的方法。当表达式执行方法引用时,数据绑定将方法引用和所有者对象包装在监听器中,并在目标视图上设置监听器。如果表达式的值为null,则数据绑定不会创建监听器,而是设置一个空监听器。
- 监听器绑定:当事件发生时执行这些lambda表达式。数据绑定总是创建视图上设置的监听器。在分派事件时,监听器执行lambda表达式。
方法引用
事件可以直接绑定到处理程序方法,类似于android:onClick
可以分配给活动中的方法的方式。与View
onClick
属性相比的一个主要优点 是表达式在编译时处理,因此如果该方法不存在或其签名不正确,则会收到编译时错误。
方法引用和监听器绑定之间的主要区别在于,实际的监听器实现是在绑定数据时创建的,而不是在触发事件时创建的。如果希望在事件发生时执行表达式,则应该使用监听器绑定。
要将事件分配给其处理程序,请使用普通绑定表达式,其值为要调用的方法名称。例如,请考虑以下示例布局数据对象:
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插件。
在方法引用中,方法的参数必须与事件侦听器的参数匹配。在侦听器绑定中,只有您的返回值必须与侦听器的预期返回值匹配(除非它预期为void)。例如,考虑下面的presenter类,它有onSaveClick()方法:
public class Presenter {
public void onSaveClick(Task task){}
}
然后,您可以将click事件绑定到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和线程安全性。
在上面的示例中,我们尚未定义view
传递给的参数onClick(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)}" />
如果您正在侦听的事件返回的值的类型不是void,那么表达式也必须返回相同类型的值。例如,如果您想要监听长时间单击事件,您的表达式应该返回一个布尔值。
public class Presenter {
public boolean onLongClick(View view, Task task) { }
}
android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"
如果由于null
对象而无法计算表达式,则数据绑定将返回该类型的默认值。例如,null
对于参考类型,0
for int
,false
for boolean
等。
如果需要使用条件表达式(例如,三元),可以使用void作为符号。
android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"
避免复杂的监听
监听器表达式非常强大,可以使您的代码非常容易阅读。另一方面,包含复杂表达式的监听器使您的布局难以阅读和维护。这些表达式应该像将可用数据从UI传递到回调方法一样简单。您应该在从侦听器表达式调用的回调方法中实现任何业务逻辑。
Imports, variables, and includes
数据绑定库提供诸imports, variables, and includes之类的功能。imports使在布局文件中引用类变得很容易。variables允许您描述可用于绑定表达式的属性。includes让您在整个应用中复用复杂的布局
Imports
Imports允许您轻松引用布局文件中的类,就像在代码中一样。import
可以在data
元素内使用零个或多个元素。以下代码示例将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}"/>
Type aliases
当存在类名冲突时,可以将其中一个类重命名为别名。以下示例将com.example.real.estate
包中的View
类 重命名为Vista
:
<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.*
会自动导入。
variables
你可以在data
元素中使用多个variable
元素。每个variable
元素都代表一个属性,该属性可以在布局中设置,以便在布局文件中的表达式中使用。下面的示例声明了用户、图像和注意变量:
<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 collection,那么它应该反映在类型中。如果变量是不实现可Observable
接口的基类或接口,则不观察变量。
当不同配置有不同的布局文件时(例如,横向或纵向),变量就会被组合起来。这些布局文件之间不能有冲突的变量定义。
生成的绑定类具有用于每个所描述的变量的setter和getter。变量采用默认的代码值,直到调用setter null
为参考类型,0
for int
,false
for boolean
等。
会生成一个名为context的特殊变量,以便根据需要在绑定表达式中使用。context的值是根视图的getContext()方法中的上下文对象。上下文变量由具有该名称的显式变量声明覆盖。
includes
通过使用属性中的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>