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运行应用。

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

6d49a79195387b40.png

此屏幕显示一些数据,并允许用户点击按钮递增计数器并更新进度条。它使用 SimpleViewModel。打开它并查看一下。

SimpleViewModel 类公开以下内容:

  • 名和姓
  • 点赞数
  • 描述流行程度的值

SimpleViewModel 还允许用户使用 onLike() 方法递增点赞数。

虽然 的功能不是最有趣的,但 SimpleViewModel 足够用于此练习。另一方面,在 PlainOldActivity 类中找到的 UI 实现存在一些问题:

  • 它多次调用 findViewById()。这不仅速度慢,而且也不安全,因为它在编译时未经检查。如果你传递给 findViewById() 的 ID 错误,应用将在运行时崩溃。
  • 它在 onCreate() 中设置初始值。最好设置自动设置的良好默认值。
  • 它在 XML 布局声明的 Button 元素中使用 android:onClick 属性,这也是不安全的:如果你的活动中未实现 onLike() 方法(或已重命名),则应用将在运行时崩溃。
  • 它有很多代码。活动和片段往往会快速增长,因此最好尽可能多地将代码移出它们。此外,活动和片段中的代码难以测试和维护。

使用数据绑定库,你可以通过将逻辑从活动移到可重用且易于测试的位置来解决所有这些问题。

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> 标记将包含布局变量

布局变量用于编写布局表达式。布局表达式位于元素属性的值中,它们使用 @{expression} 格式。以下是一些示例:

// 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 框架调用来简化活动的代码

以下是一些示例:

// 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 是一个字符串,所以数据绑定将知道如何在 TextView 中设置该值。你稍后将学习如何处理不同的布局表达式类型和属性。

  • plain_lastName TextView 执行相同的操作。
        <TextView
                android:id="@+id/plain_lastname"
                android:text="@{lastName}"
        ... />

你可以在 plain_activity_solution_2.xml 中找到这些操作的结果。

现在我们需要修改 Activity,以便它能够正确地膨胀数据绑定布局。

5. 更改膨胀并从活动中移除 UI 调用

布局已准备好,但现在需要对活动进行一些更改。打开 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 属性使用了不安全机制,当点击视图时,会调用活动或片段中的 onLike() 方法。如果不存在具有该精确签名的方法,则应用会崩溃。

新的方法更安全,因为它在编译时经过检查,并使用 lambda 表达式来调用 view model 的 onLike() 方法。

你可以在 plain_activity_solution_3.xml 中找到这些操作的结果。

现在,从活动中移除不需要的内容:

    binding.name = "Your name"
    binding.lastName = "Your last name"

替换为

    binding.viewmodel = viewModel
  1. 移除活动中的 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.

使用新的LiveData

    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. 点赞数通过可观察的整数从视图模型中公开,并绑定到文本视图,以便在它更改时自动刷新。

到目前为止,您使用了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布局更有意义。您一定会成功!

现在查看示例以了解更高级的用法,并查看文档以了解完整情况。