Android Kotlin 基础知识 02.4:数据绑定基础知识

此 Codelab 是“Android Kotlin 基础知识”课程的一部分。如果您按顺序学习这些 Codelab,您将会充分发掘此课程的价值。“Android Kotlin 基础知识”Codelab 着陆页列出了所有课程 Codelab。

简介

在本课程之前的 Codelab 中,您曾使用 findViewById() 函数来获取对视图的引用。当应用具有复杂的视图层次结构时,findViewById() 的开销很大,会减慢应用的速度,因为 Android 会从根开始遍历视图层次结构,直到找到所需的视图。幸运的是,我们有一个更好的办法。

为了在视图中设置数据,您使用了字符串资源并从 activity 中设置了数据。如果视图了解数据,效率会更高。幸运的是,这可以实现。

在此 Codelab 中,您将学习如何使用数据绑定来消除对 findViewById() 的需求。您还将学习如何使用数据绑定直接从视图访问数据。

您应当已掌握的内容

您应熟悉以下内容:

  • 什么是 activity,以及如何在 onCreate() 中设置具有布局的 activity。
  • 创建文本视图并设置文本视图显示的文本。
  • 使用 findViewById() 获取对视图的引用。
  • 为视图创建和修改基本 XML 布局。

学习内容

  • 如何使用数据绑定库消除对 findViewById() 的低效调用。
  • 如何直接从 XML 访问应用数据。

您将执行的操作

  • 修改应用以使用数据绑定而非 findViewById(),并直接从布局 XML 文件访问数据。

在此 Codelab 中,您将从 AboutMe 应用入手,然后更改该应用以使用数据绑定。完成后,应用看起来将完全一样!

AboutMe 应用的用途如下:

  • 当用户打开应用时,应用会显示一个名称、一个用于输入昵称的字段、一个完成按钮、一个星形图片和可滚动的文本。
  • 用户可以输入昵称,然后点按完成按钮。可编辑的字段和按钮会被一个显示所输入昵称的文本视图取代。


您可以使用在上一个 Codelab 中构建的代码,也可以从 GitHub 下载 AboutMeDataBinding-Starter 代码。

您在之前的 Codelab 中编写的代码使用 findViewById() 函数来获取对视图的引用。

每次在视图创建或重新创建后使用 findViewById() 搜索视图时,Android 系统都会在运行时遍历视图层次结构以找到该视图。如果您的应用只有少量视图,这不会造成问题。不过,生产应用可能在一个布局中包含数十个视图,即使设计得再好,也会有嵌套视图。

假设有一个线性布局,其中包含一个滚动视图,而该滚动视图又包含一个文本视图。对于大型或深层视图层次结构,查找视图可能需要足够的时间,以至于用户会明显感觉到应用变慢。将视图缓存在变量中会有所帮助,但您仍然必须在每个命名空间中为每个视图初始化一个变量。如果视图很多,并且有多个 activity,那么这也会增加费用。

一种解决方案是创建一个包含对每个视图的引用的对象。此对象(称为 Binding 对象)可供整个应用使用。此技术称为数据绑定。为应用创建绑定对象后,您可以通过该对象访问视图和其他数据,而无需遍历视图层次结构或搜索数据。

数据绑定具有以下优势:

  • 与使用 findByView() 的代码相比,代码更短、更清楚易懂且更易于维护。
  • 数据和视图清晰分离。在本课程的后续内容中,数据绑定的这一优势将变得越来越重要。
  • Android 系统只会遍历一次视图层次结构来获取每个视图,并且此过程发生在应用启动期间,而不是在用户与应用互动时的运行时。
  • 您可以在访问视图时获得类型安全。(类型安全表示编译器会在编译时验证类型,并且它会在您尝试为变量指定错误类型时抛出错误。)

在此任务中,您将设置数据绑定,并使用数据绑定将对 findViewById() 的调用替换为对绑定对象的调用。

第 1 步:启用数据绑定

如需使用数据绑定,您需要在 Gradle 文件中启用数据绑定,因为默认情况下数据绑定处于停用状态。这是因为数据绑定会增加编译时间,并可能会影响应用启动时间。

  1. 如果您没有之前 Codelab 中的 AboutMe 应用,请从 GitHub 获取 AboutMeDataBinding-Starter 代码。在 Android Studio 中打开该文件。
  2. 打开 build.gradle (Module: app) 文件。
  3. android 部分中,在右花括号之前,添加一个 dataBinding 部分,并将 enabled 设置为 true
dataBinding {
    enabled = true
}
  1. 收到提示时,同步项目。如果您未收到提示,请依次选择 File > Sync Project with Gradle Files
  2. 您可以运行应用,但不会看到任何变化。

第 2 步:更改布局文件,使其可与数据绑定搭配使用

如需使用数据绑定,您需要使用 <layout> 标记封装 XML 布局。这样一来,根类就不再是视图组,而是包含视图组和视图的布局。然后,绑定对象可以了解布局及其中的视图。

  1. 打开 activity_main.xml 文件。
  2. 切换到文本标签页。
  3. 添加 <layout></layout> 作为 <LinearLayout> 周围的最外层标记。
<layout>
   <LinearLayout ... >
   ...
   </LinearLayout>
</layout>
  1. 选择 Code > Reformat code 以修正代码缩进。

    布局的命名空间声明必须位于最外层的标记中。
  1. <LinearLayout> 中剪切命名空间声明,然后将其粘贴到 <layout> 标记中。起始 <layout> 标记应如下所示,并且 <LinearLayout> 标记应仅包含视图属性。
<layout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto">
  1. 构建并运行应用,以验证您是否正确完成了此操作。

第 3 步:在主 activity 中创建绑定对象

向主 activity 添加对绑定对象的引用,以便您可以使用该引用来访问视图:

  1. 打开 MainActivity.kt 文件。
  2. onCreate() 之前,在顶层为绑定对象创建一个变量。此变量通常称为 binding

    binding 的类型(即 ActivityMainBinding 类)由编译器专门为此主 activity 创建。该名称衍生自布局文件的名称,即 activity_main + Binding
private lateinit var binding: ActivityMainBinding
  1. 如果 Android Studio 提示,请导入 ActivityMainBinding。如果您未收到提示,请点击 ActivityMainBinding,然后按 Alt+Enter(在 Mac 上,按 Option+Enter)以导入此缺失的类。(如需了解更多键盘快捷键,请参阅键盘快捷键。)

    import 语句应类似于下图所示的语句。
import com.example.android.aboutme.databinding.ActivityMainBinding

接下来,您将当前 setContentView() 函数替换为执行以下操作的指令:

  • 创建绑定对象。
  • 使用 DataBindingUtil 类中的 setContentView() 函数将 activity_main 布局与 MainActivity 相关联。此 setContentView() 函数还会处理视图的一些数据绑定设置。
  1. onCreate() 中,将 setContentView() 调用替换为以下代码行。
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
  1. 导入 DataBindingUtil
import androidx.databinding.DataBindingUtil

第 4 步:使用绑定对象替换对 findViewById() 的所有调用

您现在可以将对 findViewById() 的所有调用替换为对绑定对象中视图的引用。创建绑定对象时,编译器会根据布局中视图的 ID 生成绑定对象中视图的名称,并将其转换为驼峰式命名法。因此,举例来说,如果绑定对象中的 done_buttondoneButton,则 nickname_edit 会变为 nicknameEdit,而 nickname_text 会变为 nicknameText

  1. onCreate() 中,将使用 findViewById() 查找 done_button 的代码替换为引用绑定对象中按钮的代码。

    将以下代码:findViewById<Button>(R.id.done_button)
    替换为:binding.doneButton

    用于在 onCreate() 中设置点击监听器的完成代码应如下所示。
binding.doneButton.setOnClickListener {
   addNickname(it)
}
  1. addNickname() 函数中对 findViewById() 的所有调用执行相同操作。
    将所有出现的 findViewById<View>(R.id.id_view) 替换为 binding.idView。请按以下方式操作:
  • 删除 editTextnicknameTextView 变量的定义及其对 findViewById() 的调用。这会导致出现错误。
  • 通过从 binding 对象(而非已删除的变量)获取 nicknameTextnicknameEditdoneButton 视图来修复这些错误。
  • view.visibility 替换为 binding.doneButton.visibility。使用 binding.doneButton 而不是传入的 view 可使代码更一致。

    结果是以下代码:
binding.nicknameText.text = binding.nicknameEdit.text
binding.nicknameEdit.visibility = View.GONE
binding.doneButton.visibility = View.GONE
binding.nicknameText.visibility = View.VISIBLE
  • 功能没有变化。(可选)您现在可以移除 view 参数,并将 view 的所有用法更新为在此函数内使用 binding.doneButton
  1. nicknameText 需要 String,而 nicknameEdit.textEditable。使用数据绑定时,必须将 Editable 显式转换为 String
binding.nicknameText.text = binding.nicknameEdit.text.toString()
  1. 您可以删除灰显的导入项。
  2. 使用 apply{} 将函数 Kotlin 化。
binding.apply {
   nicknameText.text = nicknameEdit.text.toString()
   nicknameEdit.visibility = View.GONE
   doneButton.visibility = View.GONE
   nicknameText.visibility = View.VISIBLE
}
  1. 构建并运行应用,此时应用的外观和运行方式应该与之前完全一样。

您可以利用数据绑定功能,直接向视图提供数据类。此技巧可简化代码,对于处理更复杂的情况非常有用。

在此示例中,您将为名称和昵称创建一个数据类,而不是使用字符串资源来设置名称和昵称。您可以使用数据绑定功能,使视图能够访问数据类。

第 1 步:创建 MyName 数据类

  1. 在 Android Studio 的 java 目录中,打开 MyName.kt 文件。如果您没有此文件,请创建一个名为 MyName.kt 的新 Kotlin 文件。
  2. 为名称和昵称定义数据类。使用空字符串作为默认值。
data class MyName(var name: String = "", var nickname: String = "")

第 2 步:向布局添加数据

activity_main.xml 文件中,名称目前是在 TextView 中通过字符串资源设置的。您需要将对名称的引用替换为对数据类中数据的引用。

  1. 文本标签页中打开 activity_main.xml
  2. 在布局顶部,在 <layout><LinearLayout> 标记之间插入 <data></data> 标记。您可以在此处将视图与数据相关联。
<data>
  
</data>

在数据标记内,您可以声明用于保存对类的引用的命名变量。

  1. <data> 标记内,添加 <variable> 标记。
  2. 添加一个 name 参数,使变量的名称为 "myName"。添加一个 type 参数,并将类型设置为 MyName 数据类的完全限定名称(软件包名称 + 变量名称)。
<variable
       name="myName"
       type="com.example.android.aboutme.MyName" />

现在,您可以引用 myName 变量,而不是使用字符串资源作为名称。

  1. android:text="@string/name" 替换为以下代码。

@={} 是一项指令,用于获取大括号内引用的数据。

myName 引用您之前定义的 myName 变量,该变量指向 myName 数据类并从该类中提取 name 属性。

android:text="@={myName.name}"

第 3 步:创建数据

现在,您已在布局文件中添加了对数据的引用。接下来,您将创建实际数据。

  1. 打开 MainActivity.kt 文件。
  2. onCreate() 上方,创建一个私有变量,按照惯例也将其命名为 myName。为变量分配 MyName 数据类的实例,并传入名称。
private val myName: MyName = MyName("Aleks Haecky")
  1. onCreate() 中,将布局文件中的 myName 变量的值设置为您刚刚声明的 myName 变量的值。您无法直接在 XML 中访问该变量。您需要通过绑定对象访问它。
binding.myName = myName
  1. 这可能会显示错误,因为您需要在做出更改后刷新绑定对象。构建应用后,该错误应该会消失。

第 4 步:在 TextView 中使用数据类来表示昵称

最后一步是也使用数据类来表示 TextView 中的昵称。

  1. 打开 activity_main.xml
  2. nickname_text 文本视图中,添加 text 属性。在数据类中引用 nickname,如下所示。
android:text="@={myName.nickname}"
  1. ActivityMain 中,将
    nicknameText.text = nicknameEdit.text.toString()
    替换为用于在 myName 变量中设置昵称的代码。
myName?.nickname = nicknameEdit.text.toString()

设置昵称后,您希望代码使用新数据刷新界面。为此,您必须使所有绑定表达式失效,以便使用正确的数据重新创建它们。

  1. 在设置昵称后添加 invalidateAll(),以便使用更新后的绑定对象中的值刷新界面。
binding.apply {
   myName?.nickname = nicknameEdit.text.toString()
   invalidateAll()
   ...
}
  1. 构建并运行应用,此时应用应该像以前一样正常运行。

Android Studio 项目:AboutMeDataBinding

使用数据绑定替换对 findViewById() 的调用的步骤:

  1. build.gradle 文件的 android 部分中启用数据绑定:
    dataBinding { enabled = true }
  2. 在 XML 布局中使用 <layout> 作为根视图。
  3. 定义绑定变量:
    private lateinit var binding: ActivityMainBinding
  4. MainActivity 中创建一个绑定对象,替换 setContentView
    binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
  5. 将对 findViewById() 的调用替换为对绑定对象中视图的引用。例如:
    findViewById<Button>(R.id.done_button) ⇒ binding.doneButton
    (在此示例中,视图的名称是根据 XML 中视图的 id 以驼峰命名法生成的。)

将视图绑定到数据的步骤:

  1. 为您的数据创建一个数据类。
  2. <layout> 标记内添加 <data> 代码块。
  3. 定义一个具有名称和数据类类型的 <variable>
<data>
   <variable
       name="myName"
       type="com.example.android.aboutme.MyName" />
</data>
  1. MainActivity 中,创建一个包含数据类实例的变量。例如:
    private val myName: MyName = MyName("Aleks Haecky")
  1. 在绑定对象中,将变量设置为您刚刚创建的变量:
    binding.myName = myName
  1. 在 XML 中,将视图的内容设置为您在 <data> 块中定义的变量。使用点分表示法访问数据类中的数据。
    android:text="@={myName.name}"

Udacity 课程:

Android 开发者文档:

此部分列出了在由讲师主导的课程中,学生学习此 Codelab 后可能需要完成的家庭作业。讲师自行决定是否执行以下操作:

  • 根据需要布置作业。
  • 告知学生如何提交家庭作业。
  • 给家庭作业评分。

讲师可以酌情采纳这些建议,并且可以自由布置自己认为合适的任何其他家庭作业。

如果您是在自学此 Codelab,可随时通过这些家庭作业来检测您的知识掌握情况。

回答以下问题

问题 1

为什么要尽量减少对 findViewById() 的显式和隐式调用?

  • 每次调用 findViewById() 时,它都会遍历视图层次结构。
  • findViewById() 在主线程或界面线程中运行。
  • 此类调用会减慢界面的响应速度。
  • 您的应用崩溃的可能性较低。

问题 2

您会如何描述数据绑定?

例如,您可以就数据绑定提出以下问题:

  • 数据绑定的核心思想是在编译时创建一个对象,将两段相距较远的信息连接/映射/绑定在一起,这样您就不必在运行时查找数据。
  • 向您显示这些绑定的对象称为绑定对象
  • 绑定对象由编译器创建。

问题 3

以下哪一项不是数据绑定的优势?

  • 代码更短、更清楚易懂且更易于维护。
  • 数据和视图清晰分离。
  • Android 系统仅遍历一次视图层次结构即可获取每个视图。
  • 调用 findViewById() 会生成编译器错误。
  • 用于访问视图的类型安全

问题 4

<layout> 代码的功能是什么?

  • 您可以在布局中将其封装在根视图周围。
  • 系统会为布局中的所有视图创建绑定。
  • 它用于指定使用数据绑定的 XML 布局中的顶级视图。
  • 您可以在 <layout> 内使用 <data> 标记将变量绑定到数据类。

问题 5

以下哪一项是在 XML 布局中引用绑定数据的正确方法?

  • android:text="@={myDataClass.property}"
  • android:text="@={myDataClass}"
  • android:text="@={myDataClass.property.toString()}"
  • android:text="@={myDataClass.bound_data.property}"

开始学习下一课:3.1:创建 fragment

如需本课程中其他 Codelab 的链接,请参阅“Android Kotlin 基础知识”Codelab 着陆页