此 Codelab 是“Android Kotlin 基础知识”课程的一部分。如果您按顺序学习这些 Codelab,您将会充分发掘此课程的价值。“Android Kotlin 基础知识”Codelab 着陆页列出了所有课程 Codelab。
简介
在上一个 Codelab 中,您更新了 TrackMySleepQuality 应用,以在 RecyclerView 中显示与睡眠质量相关的数据。您在构建首个 RecyclerView 时学到的方法足以处理大多数 RecyclerViews,用于显示源数据变化不大的简单列表。不过,有很多方法可以让 RecyclerView 更高效地处理大型列表。这些方法可使您的代码更易于维护和扩展,以处理复杂的列表和网格。
在此 Codelab 中,您将在上一个 Codelab 的睡眠跟踪器应用的基础上进行进一步构建。您将学习一种更有效的方法来更新睡眠数据列表。您还将学习如何将数据绑定与 RecyclerView 结合使用。如果您没有上一个 Codelab 的应用,可以下载此 Codelab 的起始代码。
您应当已掌握的内容
- 使用 activity、fragment 和 view 构建基本界面。
- 在 fragment 之间导航,并使用
safeArgs在 fragment 之间传递数据。 - 视图模型、视图模型工厂、转换以及
LiveData及其观察者。 - 如何创建
Room数据库,创建 DAO 和定义实体。 - 如何将协程用于数据库任务和其他长时间运行的任务。
- 如何使用
Adapter、ViewHolder和项布局实现基本RecyclerView。
学习内容
- 如何使用
DiffUtil,这个实用程序来计算两个列表之间的差异,以便高效地更新RecyclerView显示的列表。 - 如何将数据绑定与
RecyclerView结合使用。 - 如何使用绑定适配器来转换数据。
实践内容
- 在本系列上一个 Codelab 的 TrackMySleepQuality 应用的基础上进行进一步构建。
- 更新
SleepNightAdapter,以便使用DiffUtil高效地更新列表。 - 使用绑定适配器转换数据,从而为
RecyclerView实现数据绑定。
此睡眠跟踪器应用有两个屏幕,分别以 fragment 表示,如下图所示。
|
|
左侧所示的第一个屏幕包含用于开始和停止睡眠质量跟踪的按钮。这个屏幕会显示用户的一些睡眠数据。Clear 按钮用于永久删除应用针对用户收集的所有数据。右侧所示的第二个屏幕用于选择睡眠质量评分。
此应用的架构采用一个界面控制器、ViewModel 和 LiveData,以及一个用于保留睡眠数据的 Room 数据库。

睡眠数据显示在 RecyclerView 中。在此 Codelab 中,您将为 RecyclerView 构建 DiffUtil 和数据绑定部分。完成此 Codelab 后,您的应用看起来会和之前完全一样,但会变得更高效,而且更易于扩展和维护。
您可以继续使用上一个 Codelab 的睡眠跟踪器应用,也可以从 GitHub 下载 RecyclerViewDiffUtilDataBinding-Starter 应用。
- 视需要从 GitHub 下载 RecyclerViewDiffUtilDataBinding-Starter 应用,并在 Android Studio 中打开项目。
- 运行应用。
- 打开
SleepNightAdapter.kt文件。 - 检查代码,熟悉应用的结构。参阅下图,简要回顾如何将
RecyclerView与适配器模式结合使用,以向用户显示睡眠数据。

- 应用根据用户输入创建
SleepNight对象列表。每个SleepNight对象表示一个夜晚以及用户该晚睡眠的时长和质量。 SleepNightAdapter会将SleepNight对象的列表调整为RecyclerView可以使用和显示的内容。SleepNightAdapter适配器会生成ViewHolders,其中包含 RecyclerView 用于显示数据的视图、数据和元数据信息。RecyclerView使用SleepNightAdapter来确定要显示的项数 (getItemCount())。RecyclerView使用onCreateViewHolder()和onBindViewHolder()获取与要显示的数据绑定的 ViewHolder。
notifyDataSetChanged() 方法效率低下
为了告知 RecyclerView 列表中的某个项已更改且需要更新,当前代码会在 SleepNightAdapter 中调用 notifyDataSetChanged(),如下所示。
var data = listOf<SleepNight>()
set(value) {
field = value
notifyDataSetChanged()
}
但是,notifyDataSetChanged() 会告知 RecyclerView 整个列表可能无效。因此,RecyclerView 会重新绑定并重新绘制列表中的每个项,包括屏幕上看不到的项。这是一项既繁重又不必要的工作。对于较大或复杂的列表,这个过程可能需要较长时间,以至于在用户滚动浏览列表时,屏幕会闪烁或卡顿。
要解决此问题,您可以确切地告诉 RecyclerView 发生了什么更改。然后,RecyclerView 便可仅更新屏幕上已经发生更改的视图。
RecyclerView 拥有一个用于更新单个元素的功能丰富的 API。您可以使用 notifyItemChanged() 告知 RecyclerView 某个项发生了更改,并且您可以对添加、移除或移动的项使用类似的函数。您可以全部手动完成,但这样任务就会很繁重,并且可能需要使用大量代码。
幸运的是,我们有一个更好的办法。
DiffUtil 很高效并可为您完成繁重工作
RecyclerView 有一个名为 DiffUtil 的类,用于计算两个列表之间的差异。DiffUtil 会接受一个旧列表和一个新列表,并确定二者有何不同。它会查找已添加、移除或更改的项。然后,它会使用一种算法(名为 Eugene W. Myers 差分算法),来确定要生成新列表,需要对旧列表做出的最小更改量。
在 DiffUtil 确定了更改内容后,RecyclerView 可以根据这些信息仅更新已更改、添加、移除或移动的项,这比重做整个列表要高效得多。
在此任务中,您需要升级 SleepNightAdapter 以使用 DiffUtil 优化 RecyclerView,以处理数据更改。
第 1 步:实现 SleepNightDiffCallback
为了使用 DiffUtil 类的功能,请扩展 DiffUtil.ItemCallback。
- 打开
SleepNightAdapter.kt。 - 在
SleepNightAdapter的完整类定义下方,创建一个名为SleepNightDiffCallback的新顶级类,用于扩展DiffUtil.ItemCallback。以通用参数的形式传递SleepNight。
class SleepNightDiffCallback : DiffUtil.ItemCallback<SleepNight>() {
}
- 将光标放在
SleepNightDiffCallback类名称上。 - 按
Alt+Enter(在 Mac 上,按Option+Enter)并选择 Implement Members。 - 在打开的对话框中,按住 Shift 键并点击鼠标左键以选择
areItemsTheSame()和areContentsTheSame()方法,然后点击 OK。
此操作会针对这两个方法在 SleepNightDiffCallback 中生成桩,如下所示。DiffUtil 使用这两种方法来确定列表和项的具体更改。
override fun areItemsTheSame(oldItem: SleepNight, newItem: SleepNight): Boolean {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun areContentsTheSame(oldItem: SleepNight, newItem: SleepNight): Boolean {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
- 在
areItemsTheSame()中,将TODO替换为用于测试两个传入SleepNight项oldItem和newItem是否相同的代码。如果这两个项具有相同的nightId,则表明它们是相同的,因此返回true。否则返回false。DiffUtil使用此测试来帮助发现是否已添加、移除或移动某个项。
override fun areItemsTheSame(oldItem: SleepNight, newItem: SleepNight): Boolean {
return oldItem.nightId == newItem.nightId
}
- 在
areContentsTheSame()中,检查oldItem和newItem是否包含相同的数据;即判断它们是否相等。由于SleepNight是一个数据类,此相等性检查将检查所有字段。Data类会自动为您定义equals和一些其他方法。如果oldItem和newItem之间存在差异,此代码会告知DiffUtil相应项已更新。
override fun areContentsTheSame(oldItem: SleepNight, newItem: SleepNight): Boolean {
return oldItem == newItem
}
通常使用 RecyclerView 来显示会发生变化的列表。RecyclerView 提供适配器类 ListAdapter,可帮助您构建由列表支持的 RecyclerView 适配器。
ListAdapter 会为您跟踪列表,并在列表更新时通知适配器。
第 1 步:更改适配器以扩展 ListAdapter
- 在
SleepNightAdapter.kt文件中,更改SleepNightAdapter的类签名以扩展ListAdapter。 - 如果出现提示,请导入
androidx.recyclerview.widget.ListAdapter。 - 将
SleepNight作为第一个参数添加到ListAdapter中SleepNightAdapter.ViewHolder之前。 - 将
SleepNightDiffCallback()作为参数添加到构造函数中。ListAdapter将利用此参数确定列表中的更改内容。完成后的SleepNightAdapter类签名应如下所示。
class SleepNightAdapter : ListAdapter<SleepNight, SleepNightAdapter.ViewHolder>(SleepNightDiffCallback()) {
- 在
SleepNightAdapter类中,删除data字段,包括 setter。您已不再需要它,因为ListAdapter会为您跟踪列表。 - 删除
getItemCount()的替换方法,因为ListAdapter为您实现了此方法。 - 如需消除
onBindViewHolder()中的错误,请更改item变量。调用ListAdapter提供的getItem(position)方法,而不要使用data来获取item。
val item = getItem(position)
第 2 步:使用 submitList() 及时更新列表
在有已更改的列表时,您的代码需要告知 ListAdapter。ListAdapter 提供了一个名为 submitList() 的方法,用于告知 ListAdapter 列表有新版本。调用此方法时,ListAdapter 会将新列表与旧列表进行差异比较,并检测已添加、移除、移动或更改的项。然后,ListAdapter 会更新 RecyclerView 所显示的项。
- 打开
SleepTrackerFragment.kt。 - 在
sleepTrackerViewModel内的观察器上,在onCreateView()中找到引用您已删除的data变量的错误。 - 将
adapter.data = it替换为对adapter.submitList(it)的调用。更新后的代码如下所示。
sleepTrackerViewModel.nights.observe(viewLifecycleOwner, Observer {
it?.let {
adapter.submitList(it)
}
})
- 运行您的应用。您可能需要导入 findNavController。您可能会注意到,您的应用运行速度变快了,然而如果列表太小,这个变化可能不明显。
在此任务中,您需要使用与之前 Codelab 相同的方法来设置数据绑定,并消除对 findViewById() 的调用。
第 1 步:向布局文件添加数据绑定
- 在 Code 标签页中打开
list_item_sleep_night.xml布局文件。 - 将光标放在
ConstraintLayout标签上,然后按Alt+Enter(在 Mac 上,按Option+Enter)。系统随即会打开 intent 菜单(“quick fix”菜单)。 - 选择 Convert to data binding layout。这会将布局封装到
<layout>中,并在其中添加<data>标签。 - 根据需要滚动回顶部,并在
<data>标签内声明一个名为sleep的变量。 - 将其
type设为SleepNight的完全限定名称com.example.android.trackmysleepquality.database.SleepNight。完成后的<data>标签应如下所示。
<data>
<variable
name="sleep"
type="com.example.android.trackmysleepquality.database.SleepNight"/>
</data>
- 如需强制创建
Binding对象,请依次选择 Build > Clean Project,然后依次选择 Build > Rebuild Project。(如果仍然存在问题,请依次选择 File > Invalidate Caches / Restart。)ListItemSleepNightBinding绑定对象以及相关代码会添加到项目生成的文件中。
第 2 步:使用数据绑定膨胀项布局
- 打开
SleepNightAdapter.kt。 - 在
companion object中,找到from(parent: ViewGroup)函数。 - 删除
view变量的声明。
要删除的代码:
val view = layoutInflater
.inflate(R.layout.list_item_sleep_night, parent, false)
- 在
view变量所在的位置,定义一个名为binding的新变量,以膨胀ListItemSleepNightBinding绑定对象,如下所示。根据需要导入绑定对象。
val binding =
ListItemSleepNightBinding.inflate(layoutInflater, parent, false)
- 在函数结尾,不要返回
view,而应返回binding。
return ViewHolder(binding)
- 要消除
binding上的错误,请将光标放在binding一词上。按Alt+Enter(在 Mac 上,按Option+Enter)打开 intent 菜单。 - 选择 Change parameter ‘itemView' type of primary constructor of class ‘ViewHolder' to ‘ListItemSleepNightBinding'。这将更新
ViewHolder类的参数类型。

- 向上滚动到
ViewHolder的类定义,以查看签名中的更改。您会看到itemView的错误,因为您在from()方法中将itemView更改为了binding。
在 ViewHolder 类定义中,右键点击 itemView 的一个发生实例,然后依次选择 Refactor > Rename。将名称更改为 binding。
- 为构造函数参数
binding添加val前缀,使其成为属性。 - 在对父类
RecyclerView.ViewHolder的调用中,将参数从binding更改为binding.root。您需要传递View,并且将binding.root作为项布局中的根ConstraintLayout。 - 完成后的类声明应如以下代码所示。
class ViewHolder private constructor(val binding: ListItemSleepNightBinding) : RecyclerView.ViewHolder(binding.root){
您还会看到对 findViewById(). 的调用的错误。您将在下一部分中修复这些错误。
第 3 步:替换 findViewById()
您现在可以更新 sleepLength、quality 和 qualityImage 属性,以使用 binding 对象代替 findViewById()。
- 将
sleepLength、qualityString和qualityImage的初始化更改为使用binding对象的视图,如下所示。此后,您的代码应该不会再显示任何错误。
val sleepLength: TextView = binding.sleepLength
val quality: TextView = binding.qualityString
val qualityImage: ImageView = binding.qualityImage
绑定对象就位后,您根本不需要定义 sleepLength、quality 和 qualityImage 属性。DataBinding 将缓存查询,因此无需声明这些属性。
- 右键点击
sleepLength、quality和qualityImage属性名称。对于每个属性,依次选择 Refactor > Inline,或按Ctrl+Alt+N(在 Mac 上按Option+Command+N)。
- 运行您的应用。(如果项目出现错误,您可能需要清理并重建项目。)
在此任务中,您需要升级应用,将数据绑定与绑定适配器结合使用,在视图中设置数据。
在上一个 Codelab 中,您使用了 Transformations 类来获取 LiveData 并生成要在文本视图中显示的格式化字符串。但是,如果您需要绑定不同类型或复杂类型的数据,您可以提供绑定适配器来帮助数据绑定功能使用这些类型。绑定适配器会获取您的数据,并将其调整为可供数据绑定功能用于绑定视图(例如文本或图片)的内容。
您需要实现三个绑定适配器,一个用于高质量图片,另外两个分别用于一个文本字段。总而言之,如需声明绑定适配器,您需要定义一种获取项和视图的方法,并用 @BindingAdapter 进行注解。在该方法的正文中,您可以实现转换。在 Kotlin 中,您可以在接收数据的视图类上将绑定适配器编写为扩展函数。
第 1 步:创建绑定适配器
请注意,您必须在此步骤中导入很多类。
- 打开
SleepNightAdapter.kt。 - 在
ViewHolder类中,找到bind()方法并注意该方法的用途。您需要获取用于计算binding.sleepLength、binding.quality和binding.qualityImage的值的代码,并在适配器中改用该代码。(目前不要更改代码,您需要在后续步骤中移动代码。) - 在
sleeptracker软件包中,创建一个名为BindingUtils.kt的新文件并打开此文件。 - 删除
BindingUtils类中的所有内容,因为您接下来要创建静态函数。
class BindingUtils {}
- 在
TextView上声明一个名为setSleepDurationFormatted的扩展函数,并传入SleepNight。此函数将作为适配器,用于计算和格式化睡眠时长。
fun TextView.setSleepDurationFormatted(item: SleepNight) {}
- 在
setSleepDurationFormatted的正文中,将数据绑定到视图,如在ViewHolder.bind()中一样。调用convertDurationToFormatted(),然后将TextView的text设置为格式化文本。(由于这是TextView上的扩展函数,您可以直接访问text属性。)
text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, context.resources)
- 如需向数据绑定功能告知此绑定适配器,请使用
@BindingAdapter为该函数添加注解。 - 此函数是用于
sleepDurationFormatted属性的适配器,因此请将sleepDurationFormatted作为参数传递给@BindingAdapter。
@BindingAdapter("sleepDurationFormatted")
- 第二个适配器根据
SleepNight对象中的值设置睡眠质量。在TextView上再创建一个名为setSleepQualityString()的扩展函数,并传入SleepNight。 - 在正文中,将数据绑定到视图,如在
ViewHolder.bind()中一样。调用convertNumericQualityToString并设置text。 - 使用
@BindingAdapter("sleepQualityString")为该函数添加注解。
@BindingAdapter("sleepQualityString")
fun TextView.setSleepQualityString(item: SleepNight) {
text = convertNumericQualityToString(item.sleepQuality, context.resources)
}
- 我们需要第三个绑定适配器,用于在图片视图上设置图片。在
ImageView上创建第三个扩展函数,调用setSleepImage,并使用ViewHolder.bind()中的代码,如下所示。
@BindingAdapter("sleepImage")
fun ImageView.setSleepImage(item: SleepNight) {
setImageResource(when (item.sleepQuality) {
0 -> R.drawable.ic_sleep_0
1 -> R.drawable.ic_sleep_1
2 -> R.drawable.ic_sleep_2
3 -> R.drawable.ic_sleep_3
4 -> R.drawable.ic_sleep_4
5 -> R.drawable.ic_sleep_5
else -> R.drawable.ic_sleep_active
})
}
您可能需要导入 convertDurationToFormatted 和 convertNumericQualityToString。
第 2 步:更新 SleepNightAdapter
- 打开
SleepNightAdapter.kt。 - 删除
bind()方法中的所有内容,因为您现在可以使用数据绑定和新的适配器来为您执行这项操作。
fun bind(item: SleepNight) {
}
- 在
bind()中,为item分配 sleep,因为您需要告知绑定对象您的新SleepNight。
binding.sleep = item
- 在该行的下方,添加
binding.executePendingBindings()。此调用是一种优化,用于要求数据绑定功能立即执行任何待处理的绑定。当您在RecyclerView中使用绑定适配器时,最好调用executePendingBindings(),因为它可以略微加快调整视图大小的过程。
binding.executePendingBindings()
第 3 步:向 XML 布局添加绑定
- 打开
list_item_sleep_night.xml。 - 在
ImageView中,添加与设置图片的绑定适配器同名的app属性。传入sleep变量,如下所示。
此属性通过适配器建立视图与绑定对象之间的连接。每当引用 sleepImage 时,适配器都会调整 SleepNight 中的数据。
app:sleepImage="@{sleep}"
- 现在,为
sleep_length和quality_string文本视图添加类似的应用属性。每当引用sleepDurationFormatted或sleepQualityString时,适配器都会调整来自SleepNight中的数据。请务必将每个属性分别放入其各自的TextView.中
app:sleepDurationFormatted="@{sleep}"
app:sleepQualityString="@{sleep}"
- 运行您的应用,其运行情况与之前完全一样。绑定适配器负责处理随着数据变化而格式化和更新视图的所有工作,从而简化
ViewHolder并为代码提供比之前更好的结构。
您针对最后几个练习显示的列表是相同的。这是有意设计的,目的是向您表明 Adapter 接口让您可以许多不同的方式设计代码架构。代码越复杂,合理设计代码架构就越重要。在正式版应用中,这些模式和其他模式均可与 RecyclerView 结合使用。这些模式都是有效的,而且分别都有各自的优势。您应选择哪一个模式取决于您要构建什么应用。
祝贺您!至此,您已掌握 Android 中的 RecyclerView 的知识。
Android Studio 项目:RecyclerViewDiffUtilDataBinding。
DiffUtil
RecyclerView有一个名为DiffUtil的类,用于计算两个列表之间的差异。DiffUtil有一个名为ItemCallBack的类,可以扩展此类以确定两个列表之间的差异。- 在
ItemCallback类中,您必须替换areItemsTheSame()和areContentsTheSame()方法。
ListAdapter
- 如需免费获取部分列表管理功能,您可以使用
ListAdapter类,而不是RecyclerView.Adapter。不过,如果您使用ListAdapter,则必须为其他布局编写您自己的适配器,所以此 Codelab 向您介绍了具体应如何操作。 - 如需在 Android Studio 中打开 intent 菜单,请将光标放在任意代码项上,然后按
Alt+Enter(在 Mac 上,按Option+Enter)。该菜单对于重构代码以及为实现各种方法创建桩特别有用。该菜单与上下文相关,因此您需要准确放置光标才能获得正确的菜单。
数据绑定
- 在项布局中使用数据绑定将数据绑定到视图。
绑定适配器
- 您之前使用了
Transformations来根据数据创建字符串。如果您需要绑定不同类型或复杂类型的数据,请提供绑定适配器来帮助数据绑定功能使用这些类型。 - 如需声明绑定适配器,请定义一个接受项和视图的方法,并为该方法添加
@BindingAdapter注解。在 Kotlin 中,您可以在View上将绑定适配器编写为扩展函数。传入适配器调整的属性的名称。例如:
@BindingAdapter("sleepDurationFormatted")
- 在 XML 布局中,设置与绑定适配器同名的
app属性。传入包含数据的变量。例如:
.app:sleepDurationFormatted="@{sleep}"
Udacity 课程:
Android 开发者文档:
其他资源:
此部分列出了在由教师指导的课程教学中,学生完成此 Codelab 后可能需要做的家庭作业。教师自行决定是否执行以下措施:
- 根据需要布置作业。
- 告知学生如何提交家庭作业。
- 给家庭作业评分。
教师可以酌情采用这些建议,并且可以自由布置自己认为合适的任何其他家庭作业。
如果您是在自学此 Codelab,可自由使用这些家庭作业来检测您的知识掌握情况。
回答以下问题
问题 1
要使用 DiffUtil,必须执行以下哪些操作?请选择所有适用的选项。
▢ 扩展 ItemCallBack 类。
▢ 替换 areItemsTheSame()。
▢ 替换 areContentsTheSame()。
▢ 使用数据绑定跟踪各个项之间的差异。
问题 2
以下关于绑定适配器的表述中,哪些是正确的?
▢ 绑定适配器是使用 @BindingAdapter 注解的函数。
▢ 使用绑定适配器让您可以将数据格式化与 ViewHolder 分开。
▢ 如果您想使用绑定适配器,则必须使用 RecyclerViewAdapter。
▢ 当需要转换复杂数据时,绑定适配器是一个很好的解决方案。
问题 3
在什么情况下应考虑使用 Transformations 而不使用绑定适配器?请选择所有适用的选项。
▢ 您的数据很简单。
▢ 您要设置字符串格式。
▢ 您的列表很长。
▢ 您的 ViewHolder 仅包含一个视图。
开始学习下一课:

