此 Codelab 是“Android Kotlin 基础知识”课程的一部分。如果您按顺序学习这些 Codelab,您将会充分发掘此课程的价值。“Android Kotlin 基础知识”Codelab 着陆页列出了所有课程 Codelab。
简介
在上一个 Codelab 中,您更新了 TrackMySleepQuality 应用,以在 RecyclerView
中显示与睡眠质量相关的数据。您在构建第一个 RecyclerView
时学到的技巧对大多数显示简单列表并不大的 RecyclerViews
而言已经足够。不过,有许多技巧可以提高 RecyclerView
对大型列表的效率,并让您的代码更易于维护和扩展,适用于复杂的列表和网格。
在此 Codelab 中,您将在上一个 Codelab 的睡眠跟踪器应用的基础上进行进一步构建。您将了解如何更新睡眠数据列表的更有效方法,以及如何使用 RecyclerView
进行数据绑定。(如果您没有上一个 Codelab 的应用,可以下载此 Codelab 的起始代码。)
您应当已掌握的内容
- 使用 activity、fragment 和视图构建基本界面。
- 在 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 中的 SleepTracker 应用,也可以从 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
),然后选择实现成员。 - 在打开的对话框中,点击鼠标左键以选择
areItemsTheSame()
和areContentsTheSame()
方法,然后点击确定。
此操作会为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)
}
})
- 运行应用。如果应用的运行规模较小,运行速度会更快,可能也不会太明显。
在此任务中,您需要使用与之前 Codelab 相同的方法来设置数据绑定,并消除对 findViewById()
的调用。
第 1 步:向布局文件添加数据绑定
- 在 Text 标签页中打开
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
。 - 在
ViewHolder
类中,找到from()
方法。 - 删除
view
变量的声明。
要删除的代码:
val view = layoutInflater
.inflate(R.layout.list_item_sleep_night, parent, false)
- 在
view
变量所在的位置,定义一个名为binding
的新变量,以膨胀ListItemSleepNightBinding
绑定对象,如下所示。根据需要导入绑定对象。
val binding =
ListItemSleepNightBinding.inflate(layoutInflater, parent, false)
- 在函数末尾,返回
binding
,而不是返回view
。
return ViewHolder(binding)
- 要消除该错误,请将光标置于
binding
一词上。按Alt+Enter
(在 Mac 上,按Option+Enter
)打开 intent 菜单。
- 选择将参数 'itemView' 的主要构造函数类型更改为 '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,或按Control+Command+N
(在 Mac 上,按Option+Command+N
)。 - 运行您的应用。(如果项目存在错误,您可能需要清理和重新构建。)
在此任务中,您需要升级应用,将数据绑定与绑定适配器结合使用,在视图中设置数据。
在上一个 Codelab 中,您使用 Transformations
类接受了 LiveData
并生成了要在文本视图中显示的格式字符串。但是,如果您需要绑定不同类型或复杂类型,可以提供绑定适配器来帮助数据绑定使用这些类型。绑定适配器会获取您的数据,并将其调整为可供数据绑定功能用于绑定视图(例如文本或图片)的内容。
您需要实现三个绑定适配器,一个用于高质量图片,另外两个分别用于一个文本字段。总而言之,如需声明绑定适配器,您需要定义一种获取项和视图的方法,并用 @BindingAdapter
进行注解。在该方法的正文中,您可以实现转换。在 Kotlin 中,您可以将绑定适配器编写为接收数据的视图类上的扩展函数。
第 1 步:创建绑定适配器
请注意,您必须在此步骤中导入多个类,而且系统不会逐个调用该类。
- 打开
SleepNightAdapater.kt
。 - 在
ViewHolder
类中,找到bind()
方法并注意该方法的用途。您需要获取用于计算binding.sleepLength
、binding.quality
和binding.qualityImage
的值的代码,并在适配器中改用该代码。(目前不要更改代码,您需要在后续步骤中移动代码。) - 在
sleeptracker
软件包中,创建并打开名为BindingUtils.kt
的文件。 - 在
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
})
}
第 2 步:更新 SleepNightAdapter
- 打开
SleepNightAdapter.kt
。 - 删除
bind()
方法中的所有内容,因为您现在可以使用数据绑定和新的适配器来为您执行这项操作。
fun bind(item: SleepNight) {
}
- 在
bind()
中,将休眠分配给item
,因为您需要向绑定对象提供有关新的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
的数据。
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
只包含一个视图。