Android 中的数据绑定

1. 开始之前

数据绑定库 是一个 Android Jetpack 库,它允许您使用声明式格式(而不是以编程方式)将 XML 布局中的 UI 组件绑定到应用中的数据源,从而减少样板代码。

先决条件

此代码实验室专为具有一定 Android 开发经验的人员设计。

您将做什么

在此代码实验室中,您将把此应用转换为数据绑定。

Contains a static data binding part showing a name (Ada) and a last name (Lovelace) and an observable data binding part with a custom binding method, a text binding adapter and a custom progressTint Binding Adapter.

该应用只有一个屏幕,显示一些静态数据和一些可观察数据,这意味着当数据更改时,UI 将自动更新。

数据由 ViewModel 提供。模型-视图-ViewModel 是一种表示层模式,它与数据绑定配合得很好。这是一个图表

30a7d03de58070bd.png

如果您还不熟悉体系结构组件库中的 ViewModel 类,可以查看 官方文档。总而言之,它是一个向视图(Activity、Fragment 等)提供 UI 状态的类。它可以在屏幕方向更改后继续存在,并充当应用中其余层的接口。

您需要什么

2. 在没有数据绑定的情况下试用应用

在此步骤中,您将下载整个代码实验室的代码,然后运行一个简单的示例应用。

$ git clone https://github.com/android/codelab-android-databinding

或者,您可以将存储库下载为 zip 文件

  1. 解压缩代码
  2. 在 Android Studio Bumblebee 中打开项目

项目打开后,点击工具栏中的 fe7c1c9c5a888292.png 运行应用。

构建完成后并将应用部署到您的设备或模拟器后,默认 Activity 将打开,如下所示

6d49a79195387b40.png

此屏幕显示了一些数据,并允许用户点击按钮递增计数器并更新进度条。它使用 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 提供了一种方便的自动执行此操作的方法:右键点击根元素,选择显示上下文操作,然后选择转换为数据绑定布局

28f07953e569646f.png

您的布局现在应如下所示

<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 中不需要的内容

  1. 替换
    binding.name = "Your name"
    binding.lastName = "Your last name"

    binding.viewmodel = viewModel
  1. 删除 Activity 中的 onLike() 方法,因为它现在已被绕过。

您可以在 PlainOldActivitySolution3 中找到这些操作的结果。

如果您运行应用,您将看到该按钮没有任何反应。这是因为您不再调用 updateLikes()。在下一节中,您将学习如何正确地实现它。

7. 观察数据

在之前的步骤中,您创建了一个静态绑定。如果您**打开 ViewModel**,您会发现namelastName只是字符串,这很好,因为它们不会改变。但是,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.

使用新的LiveDatas

    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)}"
...

如果您现在运行该应用,点赞数会按预期递增。

c0c3e724eae590e3.png

让我们回顾一下到目前为止所做的事情

  1. 姓名和姓氏作为字符串从视图模型中公开。
  2. 按钮的onClick属性通过 lambda 表达式绑定到视图模型。
  3. 点赞数通过可观察的整数从视图模型中公开,并绑定到 TextView,以便在它发生变化时自动刷新。

到目前为止,您已经使用了android:onClickandroid: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 个点赞
  • 如果已满则更改颜色

f546fe81f7218a8d.png

我们将为此创建自定义绑定适配器。

打开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}"
...

运行该应用,您会看到当您第一次点击按钮时进度条会显示出来。但是,我们仍然需要更改其值和颜色

a55e335bd9d88fc5.png

您可以在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布局中找到解决方案。

38121b90b8dd0a41.png

11. 恭喜!

现在您知道如何创建数据绑定布局、向其中添加变量和表达式、使用可观察数据以及通过自定义绑定适配器使用自定义属性使您的 XML 布局更有意义。您一定会成功的!

现在查看示例以了解更高级的用法,并查看文档以全面了解。