Android 中的数据绑定

1. 准备工作

数据绑定库是一个 Android Jetpack 库,通过此库,您可使用声明性格式而不是以编程方式将 XML 布局中的界面组件绑定到应用中的数据源,从而减少样板代码。

先决条件

本 Codelab 专为具有一定 Android 开发经验的人员而设计。

您需要执行的操作

在本 Codelab 中,您将把以下应用转换为数据绑定:

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.

此应用包含用于显示一些静态数据和一些可观察数据的单个屏幕,这意味着在这些数据发生变化时,界面将自动更新。

这些数据由 ViewModel 提供。Model-View-ViewModel 是一个表示层模式,能够很好地与数据绑定配合使用。示意图如下:

Android communicates back and forth with the View. The View observes the ViewModel and sends user actions to it.  Outside of the presentation layer there are other layers represented by interactors or a repository.

如果您还不熟悉架构组件库中的 ViewModel 类,则可以查看 官方文档。总的来说,ViewModel 是用于为视图(Activity、Fragment 等)提供界面状态的类。ViewModel 会让相关数据会在屏幕方向变化后继续存在,并充当应用中其余层的接口。

您需要具备的条件

  • Android Studio 3.4 或更高版本

2. 试用不带数据绑定的应用

在此步骤中,您需要下载整个 Codelab 的代码,然后运行简单的示例应用。

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

或者,您可以以 zip 文件的形式下载此代码库:

  1. 解压缩代码
  2. 在 Android Studio 3.4 或更高版本中打开该项目

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

在应用构建完成并且已部署到您的设备或模拟器之后,系统将打开默认 Activity,其界面如下图所示:

Screenshot

此屏幕显示了一些数据,并允许用户点击按钮,以增加计数器的计数并更新进度条。该 Activity 会使用 SimpleViewModel。可以打开看一下。

SimpleViewModel 类可显示:

  • 名字和姓氏
  • 顶的数量
  • 描述热度级别的值

SimpleViewModel 还允许用户通过 onLike() 方法增加顶的数量。

虽然 SimpleViewModel 没有非常有意思的功能,但对于本练习来说已够用。另一方面,PlainOldActivity 类中的界面实现存在多个问题:

  • 会多次调用 findViewById()。此操作不仅速度缓慢,还不安全,因为在编译时系统不会对其进行检查。如果您将错误的 ID 传递给 findViewById(),则该应用会在运行时崩溃。
  • 会在 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 提供了可以自动完成此操作的简便方法:右键点击根元素, 选择"Show Context Actions"(显示上下文操作),然后选择"Convert to data binding layout"(转换为数据绑定布局):

Android Studio screenshot

现在,您的布局应如下所示:

<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}'

通过使用布局表达式来绑定布局文件中的组件,您可以:

  • 提高应用的性能
  • 帮助避免出现内存泄漏和 null 指针异常
  • 通过移除界面框架调用来简化 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 是字符串,因此数据绑定将知道如何在 TextView 中设置该值。您稍后将学习如何处理不同的布局表达式类型和属性。

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

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

现在,我们需要修改 Activity,以便其能正确扩充数据绑定布局:

5. 更改扩充并从 Activity 中移除界面调用

布局已就绪,但现在必须在 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

2.移除 Activity 中的 onLike() 方法,因为现在我们会绕过该方法。

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

如果您运行应用,则会发现该按钮不会执行任何操作。这是因为您不再调用 updateLikes()。在下一节中,您将学习如何正确实现该操作。

7. 观察数据

在之前的步骤中,您创建了静态绑定。如果您打开 ViewModel,则会发现 namelastName 都只是字符串,这是正常的,因为它们不会发生变化。但是,用户会修改 likes 的值。

var likes =  0

当此值发生变化时,应使其成为可观察值,而不是显式更新界面。

有多种方法可以实现可观测性。您可以使用 可观察类可观察字段,或者使用首选方法,即 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 属性设置的,并且在创建 LiveData 时,您可以使用 Transformations 来依据另一个 LiveData 进行创建。此机制允许库在值发生变化时更新界面。

LiveData 是一种具有生命周期感知能力的可观察对象,因此您需要指定生命周期所有者要使用的内容。您可以在 binding 对象中执行此操作。

打开 PlainOldActivity (具体代码应如 PlainOldActivitySolution3 所示)并在 binding 对象中设置生命周期所有者:

binding.lifecycleOwner = this

如果您重新构建项目,则会发现系统未对此 Activity 进行编译。我们将从这个我们不再需要的 Activity 中直接访问 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 并移除此 Activity 中的所有专用方法及其调用。该 Activity 现在变得非常简单。

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 中找到这些操作的结果。

一般情况下,从 Activity 中移出代码对可维护性和可测试性有很大的帮助。

让我们将显示顶数量的 TextView 绑定到可观察的整数。在 plain_activity.xml 中:

        <TextView
                android:id="@+id/likes"
                android:text="@{Integer.toString(viewmodel.likes)}"
...

如果您现在运行应用,则顶的数量会按预期增加。

Screenshot of the app running correctly

我们来回顾一下到目前为止已执行的操作:

  1. 在 ViewModel 中,名字和姓氏显示为字符串。
  2. 此按钮的 onClick 属性通过 lambda 表达式绑定到 ViewModel。
  3. 顶数量通过可观察的整数显示在 ViewModel 中并绑定到 TextView,因此它会在发生变化时自动刷新。

到目前为止,您已使用 android:onClickandroid:text 等属性。在下一节中,您将了解其他属性并创建自己的属性。

8. 使用绑定适配器创建自定义属性

在您将某个字符串(或可观察字符串)绑定到某个 android:text 属性时,将出现什么情况显而易见,但是如何出现的呢?

在数据绑定库中,几乎所有的界面调用都是在称为绑定适配器的静态方法中完成的。

该库提供了大量的绑定适配器。以下是 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 个顶的时候,进度条填满
  • 填满时更改颜色

Screenshot of the app with a pink icon and full bar

我们将创建用于实现此目标的自定义绑定适配器。

打开 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,因此您可以通过更改此类型将其限制为特定类)
  • 获取整数值,且该值应当为布局表达式返回的值。
  • 如果 number 为零,则将 View 设置为"GONE"。否则,设置为"VISIBLE"。

plain_activity 布局中,找到进度条并添加 hideIfZero 属性:

    <ProgressBar
            android:id="@+id/progressBar"
            app:hideIfZero="@{viewmodel.likes}"
...

运行应用,您将看到进度条会在您首次点击按钮时显示。但是,我们仍然需要更改进度条的值和颜色:

Screenshot

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

404cfd3edb58ca5c.png

11. 您一定会取得成功!

恭喜!您已完成此 Codelab,现在您应该知道如何创建数据绑定布局、向其中添加变量和表达式、使用可观察数据,以及通过自定义绑定适配器并使用自定义属性让 XML 布局变得更有意义。

现在,您可以查看这些 示例了解更多高级用法,也可查看该 文档进行全面了解。