1. 开始之前
数据绑定库 是一个 Android Jetpack 库,它允许您使用声明式格式(而不是以编程方式)将 XML 布局中的 UI 组件绑定到应用中的数据源,从而减少样板代码。
先决条件
此代码实验室专为具有一定 Android 开发经验的人员设计。
您将做什么
在此代码实验室中,您将把此应用转换为数据绑定。
该应用只有一个屏幕,显示一些静态数据和一些可观察数据,这意味着当数据更改时,UI 将自动更新。
数据由 ViewModel 提供。模型-视图-ViewModel 是一种表示层模式,它与数据绑定配合得很好。这是一个图表
如果您还不熟悉体系结构组件库中的 ViewModel 类,可以查看 官方文档。总而言之,它是一个向视图(Activity、Fragment 等)提供 UI 状态的类。它可以在屏幕方向更改后继续存在,并充当应用中其余层的接口。
您需要什么
- Android Studio Bumblebee
2. 在没有数据绑定的情况下试用应用
在此步骤中,您将下载整个代码实验室的代码,然后运行一个简单的示例应用。
$ git clone https://github.com/android/codelab-android-databinding
或者,您可以将存储库下载为 zip 文件
项目打开后,点击工具栏中的 运行应用。
构建完成后并将应用部署到您的设备或模拟器后,默认 Activity 将打开,如下所示
此屏幕显示了一些数据,并允许用户点击按钮递增计数器并更新进度条。它使用 SimpleViewModel
。打开它并查看。
SimpleViewModel
类公开了
- 名字和姓氏
- 喜欢次数
- 描述受欢迎程度的值
SimpleViewModel
还允许用户使用 onLike()
方法递增喜欢次数。
虽然 它
的功能不是最有趣的,但 SimpleViewModel 足以
满足此练习的需要。另一方面,在 PlainOldActivity 类
中找到的 UI 实现存在一些问题
- 它多次调用
findViewById()
。这不仅速度慢,而且不安全,因为在编译时未对其进行检查。如果您传递给findViewById()
的 ID 错误,则应用将在运行时崩溃。 - 它在
onCreate()
中设置初始值。最好使用自动设置的良好默认值 - 它在 XML 布局声明的
Button
元素中使用android:onClick
属性,这也是不安全的:如果您的 Activity 中未实现onLike()
方法(或已重命名),则应用将在运行时崩溃。 - 它有很多代码。Activity 和 Fragment 往往会迅速增长,因此最好尽可能地将代码移出它们。此外,Activity 和 Fragment 中的代码难以测试和维护。
使用数据绑定库,您可以通过将逻辑从 Activity 移到可重用且更易于测试的位置来解决所有这些问题。
3. 启用数据绑定并转换布局
此项目已启用数据绑定,但当您想在自己的项目中使用它时,第一步是在将使用它的模块中启用该库
build.gradle
android {
...
buildFeatures {
dataBinding true
}
}
现在将布局转换为数据绑定布局。
打开 plain_activity.xml
。它是一个常规布局,其根元素为 ConstraintLayout
。
为了将布局转换为数据绑定,您需要将根元素包装在 <layout>
标记中。您还必须将命名空间定义(以 xmlns:
开头的属性)移动到新的根元素。
Android Studio 提供了一种方便的自动执行此操作的方法:右键点击根元素,选择显示上下文操作,然后选择转换为数据绑定布局
您的布局现在应如下所示
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
...
<data>
标记将包含布局变量。
布局变量用于编写布局表达式。布局表达式放置在元素属性的值中,并使用 @{
表达式
}
格式。以下是一些示例
// Some examples of complex layout expressions
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
通过使用布局表达式绑定布局文件中的组件,您可以
- 提高应用的性能
- 帮助防止内存泄漏和空指针异常
- 通过删除 UI 框架调用来简化 Activity 的代码
以下是一些示例
// Bind the name property of the viewmodel to the text attribute
android:text="@{viewmodel.name}"
// Bind the nameVisible property of the viewmodel to the visibility attribute
android:visibility="@{viewmodel.nameVisible}"
// Call the onLike() method on the viewmodel when the View is clicked.
android:onClick="@{() -> viewmodel.onLike()}"
查看此处提供的语言完整说明 此处。
现在,让我们绑定一些数据!
4. 创建您的第一个布局表达式
现在让我们先进行一些静态数据绑定。
- 在
<data>
标记内创建两个String
布局变量。
<data>
<variable name="name" type="String"/>
<variable name="lastName" type="String"/>
</data>
- 查找 ID 为
plain_name
的 TextView,并使用布局表达式添加android:text
属性
<TextView
android:id="@+id/plain_name"
android:text="@{name}"
... />
布局表达式以 @ 符号开头,并用花括号 { } 括起来。
因为 name
是一个 String,所以数据绑定将知道如何在 TextView 中设置该值。您将在稍后学习如何处理不同的布局表达式类型和属性。
- 对
plain_lastName
文本视图执行相同的操作
<TextView
android:id="@+id/plain_lastname"
android:text="@{lastName}"
... />
您可以在 plain_activity_solution_2.xml
中找到这些操作的结果。
现在我们需要修改 Activity,以便它能够正确地填充数据绑定布局
5. 更改填充并从 Activity 中删除 UI 调用
布局已准备就绪,但现在需要对 Activity 进行一些更改。打开 PlainOldActivity
。
因为您正在使用数据绑定布局,所以填充方式有所不同。
在 onCreate
中,替换
setContentView(R.layout.plain_activity)
为
val binding : PlainActivityBinding =
DataBindingUtil.setContentView(this, R.layout.plain_activity)
此变量的用途是什么?您需要它来设置在 <data>
块中声明的那些布局变量。绑定类由库自动生成。
要查看生成的类的外观,请打开 PlainActivitySolutionBinding
并查看。
- 现在您可以设置变量值了
binding.name = "Your name"
binding.lastName = "Your last name"
就是这样。您刚刚使用该库绑定了数据。
您可以开始删除旧代码了
- 删除
updateName()
方法,因为新的数据绑定代码现在正在查找 ID 并设置文本值。 - 删除
onCreate()
中的updateName()
调用。
您可以在 PlainOldActivitySolution2
中找到这些操作的结果。
您现在可以运行应用了。您将看到您的姓名已替换了 Ada 的姓名。
6. 处理用户事件
到目前为止,您已经学习了如何向用户显示数据,但使用数据绑定库,您还可以处理用户事件并在布局变量上调用操作。
在修改事件处理代码之前,您可以稍微清理一下布局。
- 首先,将两个变量替换为单个 ViewModel。在大多数情况下,这是最佳做法,因为它将您的表示代码和状态保留在一个位置。
<data>
<variable
name="viewmodel"
type="com.example.android.databinding.basicsample.data.SimpleViewModel"/>
</data>
不要直接访问变量,而是调用 viewmodel 属性
- 更改两个 TextView 中的布局表达式
<TextView
android:id="@+id/plain_name"
android:text="@{viewmodel.name}"
... />
<TextView
android:id="@+id/plain_lastname"
android:text="@{viewmodel.lastName}"
... />
此外,更新“喜欢”按钮的点击处理方式。
- 查找
like_button
按钮并替换
android:onClick="onLike"
为
android:onClick="@{() -> viewmodel.onLike()}"
以前的 onClick
属性使用了一种不安全机制,即在点击视图时会调用 Activity 或 Fragment 中的 onLike()
方法。如果不存在具有该精确签名的 方法,则应用会崩溃。
新方法更加安全,因为它在编译时会进行检查,并使用 lambda 表达式调用 ViewModel 的 onLike()
方法。
您可以在 plain_activity_solution_3.xml
中找到这些操作的结果。
现在,删除 Activity 中不需要的内容
- 替换
binding.name = "Your name"
binding.lastName = "Your last name"
为
binding.viewmodel = viewModel
- 删除 Activity 中的
onLike()
方法,因为它现在已被绕过。
您可以在 PlainOldActivitySolution3
中找到这些操作的结果。
如果您运行应用,您将看到该按钮没有任何反应。这是因为您不再调用 updateLikes()
。在下一节中,您将学习如何正确地实现它。
7. 观察数据
在之前的步骤中,您创建了一个静态绑定。如果您**打开 ViewModel**,您会发现name
和lastName
只是字符串,这很好,因为它们不会改变。但是,likes
会被用户修改。
var likes = 0
当此值发生变化时,不要显式更新 UI,而是使其**可观察**。
实现可观察性的方法有多种。您可以使用可观察类、可观察字段,或者首选方式,LiveData。有关此方面的完整文档请参见此处。
我们将使用ObservableField
,因为它们更简单。
替换
val name = "Grace"
val lastName = "Hopper"
var likes = 0
private set // This is to prevent external modification of the variable.
使用新的LiveData
s
private val _name = MutableLiveData("Ada")
private val _lastName = MutableLiveData("Lovelace")
private val _likes = MutableLiveData(0)
val name: LiveData<String> = _name
val lastName: LiveData<String> = _lastName
val likes: LiveData<Int> = _likes
此外,替换
fun onLike() {
likes++
}
/**
* Returns popularity in buckets: [Popularity.NORMAL],
* [Popularity.POPULAR] or [Popularity.STAR]
*/
val popularity: Popularity
get() {
return when {
likes > 9 -> Popularity.STAR
likes > 4 -> Popularity.POPULAR
else -> Popularity.NORMAL
}
}
为
// popularity is exposed as LiveData using a Transformation instead of a @Bindable property.
val popularity: LiveData<Popularity> = Transformations.map(_likes) {
when {
it > 9 -> Popularity.STAR
it > 4 -> Popularity.POPULAR
else -> Popularity.NORMAL
}
}
fun onLike() {
_likes.value = (_likes.value ?: 0) + 1
}
如您所见,LiveData 的值使用value
属性设置,您可以使用Transformations使一个 LiveData 依赖于另一个 LiveData。此机制允许库在值发生变化时更新 UI。
LiveData 是一个生命周期感知的可观察对象,因此您需要指定要使用的生命周期所有者。您可以在binding
对象中执行此操作。
打开PlainOldActivity
(它应该看起来像PlainOldActivitySolution3
)并在binding
对象中设置生命周期所有者
binding.lifecycleOwner = this
如果您重新构建项目,您会发现该活动无法编译。我们直接从活动中访问likes
,而我们不再需要它了
private fun updateLikes() {
findViewById<TextView>(R.id.likes).text = viewModel.likes.toString()
findViewById<ProgressBar>(R.id.progressBar).progress =
(viewModel.likes * 100 / 5).coerceAtMost(100)
...
打开PlainOldActivity
并删除活动中的所有私有方法及其调用。现在,活动变得尽可能简单。
class PlainOldActivity : AppCompatActivity() {
// Obtain ViewModel from ViewModelProviders
private val viewModel by lazy { ViewModelProviders.of(this).get(SimpleViewModel::class.java) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding : PlainActivityBinding =
DataBindingUtil.setContentView(this, R.layout.plain_activity)
binding.lifecycleOwner = this
binding.viewmodel = viewModel
}
}
您可以在SolutionActivity
中找到这些操作的结果。
通常,将代码移出活动对于可维护性和可测试性非常有用。
让我们将显示点赞数的TextView
绑定到可观察的整数。在plain_activity.xml中
<TextView
android:id="@+id/likes"
android:text="@{Integer.toString(viewmodel.likes)}"
...
如果您现在运行该应用,点赞数会按预期递增。
让我们回顾一下到目前为止所做的事情
- 姓名和姓氏作为字符串从视图模型中公开。
- 按钮的
onClick
属性通过 lambda 表达式绑定到视图模型。 - 点赞数通过可观察的整数从视图模型中公开,并绑定到 TextView,以便在它发生变化时自动刷新。
到目前为止,您已经使用了android:onClick
和android:text
之类的属性。在下一节中,您将了解其他属性并创建您自己的属性。
8. 使用绑定适配器创建自定义属性
当您将字符串(或可观察字符串)绑定到android:text
属性时,很明显会发生什么,但**如何**发生呢?
使用数据绑定库,几乎所有 UI 调用都在称为绑定适配器的静态方法中完成。
该库提供了大量的绑定适配器。请查看此处。这是android:text
属性的一个示例
@BindingAdapter("android:text")
public static void setText(TextView view, CharSequence text) {
// Some checks removed for clarity
view.setText(text);
}
或android:background
@BindingAdapter("android:background")
public static void setBackground(View view, Drawable drawable) {
if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
view.setBackground(drawable);
} else {
view.setBackgroundDrawable(drawable);
}
}
数据绑定中没有魔法。所有内容都在编译时解析,并且您可以查看生成的代码以读取它。
让我们处理进度条。我们希望它
- 如果点赞数为零则不可见
- 充满 5 个点赞
- 如果已满则更改颜色
我们将为此创建自定义绑定适配器。
打开utils包中的BindingAdapters.kt
文件。您在任何位置创建它们都没有关系,库会找到它们。在 Kotlin 中,可以通过向 Kotlin 文件的顶层添加函数或作为类的扩展函数来创建静态方法。
查找第一个需求的绑定适配器,hideIfZero
@BindingAdapter("app:hideIfZero")
fun hideIfZero(view: View, number: Int) {
view.visibility = if (number == 0) View.GONE else View.VISIBLE
}
此绑定适配器
- 应用于
app:hideIfZero
属性。 - 可以应用于每个 View(因为第一个参数是 View;您可以通过更改此类型将其限制为某些类)
- 获取一个整数,该整数应该是布局表达式返回的值。
- 如果数字为零,则使 View 变为 GONE。否则为 VISIBLE。
在plain_activity
布局中,查找进度条并添加hideIfZero
属性
<ProgressBar
android:id="@+id/progressBar"
app:hideIfZero="@{viewmodel.likes}"
...
运行该应用,您会看到当您第一次点击按钮时进度条会显示出来。但是,我们仍然需要更改其值和颜色
您可以在plain_activity_solution_4.xml
中找到这些步骤的结果。
9. 创建具有多个参数的绑定适配器
对于进度值,我们将使用一个绑定适配器,它获取最大值和点赞数。打开BindingAdapters
文件并查找此适配器
/**
* Sets the value of the progress bar so that 5 likes will fill it up.
*
* Showcases Binding Adapters with multiple attributes. Note that this adapter is called
* whenever any of the attribute changes.
*/
@BindingAdapter(value = ["app:progressScaled", "android:max"], requireAll = true)
fun setProgress(progressBar: ProgressBar, likes: Int, max: Int) {
progressBar.progress = (likes * max / 5).coerceAtMost(max)
}
如果任何属性缺失,则不使用此绑定适配器。这发生在编译时。该方法现在采用 3 个参数(它应用到的视图加上注释中定义的属性数量)。
requireAll
参数定义何时使用绑定适配器
- 当
true
时,所有元素都必须存在于 XML 定义中。 - 当
false
时,缺失的属性将为null
,如果为布尔值则为false
,如果为基本类型则为 0。
接下来,将属性添加到 XML 中
<ProgressBar
android:id="@+id/progressBar"
app:hideIfZero="@{viewmodel.likes}"
app:progressScaled="@{viewmodel.likes}"
android:max="@{100}"
...
我们将progressScaled
属性绑定到点赞数,并且我们只是将一个字面整数传递给max
属性。如果您不添加@{}
格式,数据绑定将无法找到正确的绑定适配器。
您可以在plain_activity_solution_5.xml
中找到这些步骤的结果。
如果您运行该应用,您会看到进度条按预期填充。
10. 练习创建绑定适配器
熟能生巧。创建
- 一个根据点赞值对进度条颜色进行着色的绑定适配器,并添加相应的属性
- 一个根据受欢迎程度显示不同图标的绑定适配器
ic_person_black_96dp
为黑色ic_whatshot_black_96dp
为浅粉色ic_whatshot_black_96dp
为亮粉色
您可以在BindingAdapters.kt
文件、SolutionActivity
文件和solution.xml
布局中找到解决方案。
11. 恭喜!
现在您知道如何创建数据绑定布局、向其中添加变量和表达式、使用可观察数据以及通过自定义绑定适配器使用自定义属性使您的 XML 布局更有意义。您一定会成功的!