此 Codelab 是“Android Kotlin 基础知识”课程的一部分。如果您按顺序学习这些 Codelab,您将会充分发掘此课程的价值。“Android Kotlin 基础知识”Codelab 着陆页列出了所有课程 Codelab。
简介
此 Codelab 会教您如何使用 RecyclerView
显示项列表。在上一个 Codelab 中,您将基于睡眠跟踪器应用进行构建,学习如何使用具有推荐架构的 RecyclerView
更好、更灵活地显示数据。
您应当已掌握的内容
您应熟悉以下内容/操作:
- 使用 activity、fragment 和视图构建基本界面 (UI)。
- 在 fragment 之间导航,并使用
safeArgs
在 fragment 之间传递数据。 - 使用视图模型、视图模型工厂、转换以及
LiveData
及其观察者。 - 创建
Room
数据库、创建 DAO 以及定义实体。 - 将协程用于数据库任务和其他长时间运行的任务。
学习内容
- 如何将
RecyclerView
与Adapter
和ViewHolder
配合使用来显示项列表。
您将执行的操作
- 更改上一课中的 TrackMySleepQuality 应用,以使用
RecyclerView
显示睡眠质量数据。
在此 Codelab 中,您将为一款用于跟踪睡眠质量的应用构建 RecyclerView
部分。该应用使用 Room
数据库来存储睡眠数据随时间的变化情况。
起始睡眠跟踪器应用有两个屏幕(由 fragment 表示),如下图所示。
左侧所示的第一个屏幕包含用于开始和停止跟踪的按钮。这个屏幕还会显示用户的所有睡眠数据。CLEAR 按钮用于永久删除应用针对用户收集的所有数据。右侧所示的第二个屏幕用于选择睡眠质量评分。
此应用采用简化的架构,其中包括一个界面控制器、ViewModel
和 LiveData
。该应用还使用 Room
数据库使睡眠数据持久化。
第一个屏幕上显示的睡眠之夜列表功能正常,但并不美观。该应用使用复杂的格式设置工具为文本视图创建文本字符串,并为质量创建数值。另外,这种设计不能按比例调整。在此 Codelab 中解决所有这些问题后,最终应用具有相同的功能,主屏幕将如下所示:
显示数据列表或数据网格是 Android 中最常见的界面任务之一。列表包含从简单到非常复杂的各种内容。文本视图列表可以显示简单的数据,例如购物清单。复杂列表(例如带注解的度假目的地列表)可以在带标题的滚动网格内向用户显示许多详细信息。
为了支持所有这些用例,Android 提供了 RecyclerView
widget。
RecyclerView
的最大优势是它对于大型列表非常有效:
- 默认情况下,
RecyclerView
仅会处理或绘制当前显示在屏幕上的项。例如,如果您的列表包含一千个元素,但只有 10 个元素可见,那么RecyclerView
仅会完成在屏幕上绘制这 10 个项的工作。当用户滚动时,RecyclerView
会确定应在屏幕上显示哪些新项,然后仅完成显示这些项的工作。 - 当某个项滚动出屏幕时,RecyclerView 会回收其视图。这表示该项内容是会滚动到屏幕上的新内容。此
RecyclerView
行为有助于节省大量处理时间,并有助于列表流畅地滚动。 - 当某个项发生变化时,
RecyclerView
无需重新绘制整个列表即可更新该项。显示复杂列表时,可显著提高效率!
在下图显示的序列中,可以看到一个填充了数据 ABC
的视图。当该视图滚动出屏幕之后,RecyclerView
会重复使用该视图来显示新数据 XYZ
。
适配器模式
如果您在使用不同电气插座的国家/地区旅行,或许知道如何使用适配器将设备插入插座。通过使用适配器,您可将一种插头转换为另一种插头,这实际上是将一个接口转换为另一个接口。
软件工程中的适配器模式可帮助对象与其他 API 配合使用。RecyclerView
使用适配器将应用数据转换为 RecyclerView
可以显示的内容,而无需更改应用存储和处理相应数据的方式。对于睡眠跟踪器应用,您需要构建一个适配器,该适配器会将 Room
数据库中的数据调整为 RecyclerView
知道如何显示的内容,而无需更改 ViewModel
。
实现 RecyclerView
如需在 RecyclerView
中显示您的数据,您需要以下几个部分:
- 要显示的数据。
- 在布局文件中定义的
RecyclerView
实例,充当视图的容器。 - 一个数据项的布局。
如果所有列表项看起来都相同,您可以针对所有项目使用相同的布局,但这不是强制性的。项布局必须与 fragment 的布局分开创建,以便一次创建一个项视图,并在其中填充数据。 - 布局管理器。
布局管理器负责处理视图中的界面组件的组织(布局)。 - ViewHolder。
ViewHolder 会扩展ViewHolder
类。它包含视图信息,用于显示项布局中的一项。ViewHolder 还会添加一些信息,供RecyclerView
用于在屏幕上高效移动视图。 - 适配器。
适配器会将您的数据连接到RecyclerView
。它会调整数据以使其显示在ViewHolder
中。RecyclerView
使用 Adapter 确定如何在屏幕上显示数据。
在此任务中,您将向布局文件添加 RecyclerView
,并设置 Adapter
以向 RecyclerView
公开睡眠数据。
第 1 步:使用 LayoutManager 添加 RecyclerView
在此步骤中,您需要将 fragment_sleep_tracker.xml
文件中的 ScrollView
替换为 RecyclerView
。
- 从 GitHub 下载 RecyclerViewFundamentals-Starter 应用。
- 构建并运行应用。请注意数据如何显示为简单文本。
- 在 Android Studio 的 Design 标签页中,打开
fragment_sleep_tracker.xml
布局文件。 - 在 Component Tree 窗格中,删除
ScrollView
。此操作还会删除ScrollView
内的TextView
。 - 在 Palette 窗格中,滚动浏览左侧的组件类型列表,找到 Containers,然后选择它。
- 将
RecyclerView
从 Palette 窗格拖动到 Component Tree 窗格中。将RecyclerView
放入ConstraintLayout
内。
- 如果屏幕上打开一个对话框,询问您是否要添加依赖项,请点击 OK,让 Android Studio 将
recyclerview
依赖项添加到您的 Gradle 文件中。此过程可能需要几秒钟的时间,然后您的应用会同步。
- 打开模块
build.gradle
文件,滚动到末尾,并记下新的依赖项,它类似于以下代码:
implementation 'androidx.recyclerview:recyclerview:1.0.0'
- 切换回
fragment_sleep_tracker.xml
。 - 在 Text 标签页中,查找如下所示的
RecyclerView
代码:
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent" />
- 为
RecyclerView
指定id
作为sleep_list
。
android:id="@+id/sleep_list"
- 放置
RecyclerView
,使其占据屏幕上ConstraintLayout
内的剩余部分。为此,请将RecyclerView
的顶部约束到 START 按钮,将底部约束到 CLEAR 按钮,并将两侧约束到相应父级。使用以下代码,在布局编辑器或 XML 中将布局宽度和高度设置为 0 dp:
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/clear_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/stop_button"
- 向
RecyclerView
XML 添加一个布局管理器。每个RecyclerView
都需要一个布局管理器,用于指示如何在列表中放置项。Android 提供了LinearLayoutManager
,默认情况下,它会以全宽行垂直排列项。
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
- 切换到 Design 标签页,并会看到添加的约束条件导致
RecyclerView
展开即可填充可用空间。
第 2 步:创建列表项布局和文本 ViewHolder
RecyclerView
只是一个容器。在此步骤中,您将为要在 RecyclerView
中显示的项创建布局和基础架构。
为了尽快获得一个能正常运行的 RecyclerView
,请先使用仅以数值形式显示睡眠质量的简化列表项。为此,您需要一个 ViewHolder - TextItemViewHolder
。您还需要一个数据视图 - TextView
。(在稍后的步骤中,您将详细了解 ViewHolder 以及如何排列所有睡眠数据。)
- 创建一个名为
text_item_view.xml
的布局文件。将哪个元素用作根元素无关紧要,因为此操作会替换模板代码。 - 在
text_item_view.xml
中,删除所有指定代码。 - 添加
TextView
,在开头和末尾添加16dp
的内边距,并将文本大小设置为24sp
。使宽度与相应父级一致,而高度则包住内容。由于此视图显示在RecyclerView
内,因此您无需将其放在ViewGroup
内。
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:textSize="24sp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
- 打开
Util.kt
。滚动到末尾,添加如下所示的定义,这将创建TextItemViewHolder
类。将代码放在该文件底部最后一个英文大括号之后。之所以将代码置于Util.kt
中,是因为此 ViewHolder 是临时的,您稍后要替换它。
class TextItemViewHolder(val textView: TextView): RecyclerView.ViewHolder(textView)
- 如果系统提示,请导入
android.widget.TextView
和androidx.recyclerview.widget.RecyclerView
。
第 3 步:创建 SleepNightAdapter
实现 RecyclerView
的核心任务是创建适配器。您的项视图有一个简单的 ViewHolder,并且每个项都有一个布局。现在,您可以创建适配器。该适配器创建一个 ViewHolder,并在其中填充可供 RecyclerView
显示的数据。
- 在
sleeptracker
软件包中,创建一个名为SleepNightAdapter
的新 Kotlin 类。 - 让
SleepNightAdapter
类扩展RecyclerView.Adapter
。这个类称为SleepNightAdapter
,因为它会将SleepNight
对象调整为RecyclerView
可以使用的东西。适配器需要知道要使用哪个 ViewHolder,因此请传入TextItemViewHolder
。在看到提示时导入必要的组件,然后您会看到错误消息,因为存在要实现的强制性方法。
class SleepNightAdapter: RecyclerView.Adapter<TextItemViewHolder>() {}
- 在
SleepNightAdapter
的顶层,创建一个listOf
SleepNight
变量来保存数据。
var data = listOf<SleepNight>()
- 在
SleepNightAdapter
中,替换getItemCount()
以返回data
中睡眠之夜列表的大小。RecyclerView
需要知道适配器要显示多少项,通过调用getItemCount()
来实现这一点。
override fun getItemCount() = data.size
- 在
SleepNightAdapter
中,替换onBindViewHolder()
函数,如下所示。RecyclerView
会调用onBindViewHolder()
函数,以显示某个列表项在指定位置的数据。因此,onBindViewHolder()
方法接受两个参数:一个 ViewHolder 和一个要绑定的数据的位置。对于此应用,存储器为TextItemViewHolder
,位置是列表中的位置。
override fun onBindViewHolder(holder: TextItemViewHolder, position: Int) {
}
- 在
onBindViewHolder()
中,为数据中位于给定位置的项创建一个变量。
val item = data[position]
- 您创建的
ViewHolder
具有一个名为textView
的属性。在onBindViewHolder()
中,将textView
的text
设置为睡眠质量数值。下面这段代码只显示了数值列表,不过您可以借助这个简单示例了解适配器如何将数据放入 ViewHolder 和屏幕中。
holder.textView.text = item.sleepQuality.toString()
- 在
SleepNightAdapter
中,替换并实现onCreateViewHolder()
,在RecyclerView
需要 ViewHolder 表示项时调用。
此函数接受两个参数并返回ViewHolder
。parent
参数是用于存储 ViewHolder 的视图组,它始终为RecyclerView
。当同一个RecyclerView
中有多个视图时,会使用viewType
参数。例如,如果您将文本视图、图片和视频的列表放到同一个RecyclerView
中,则onCreateViewHolder()
函数需要知道要使用的视图类型。
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TextItemViewHolder {
}
- 在
onCreateViewHolder()
中,创建LayoutInflater
的实例。
布局膨胀器知道如何通过 XML 布局创建视图。context
包含有关如何正确膨胀视图的信息。在 recycler 视图的适配器中,您始终传入parent
视图组(即RecyclerView
)的上下文。
val layoutInflater = LayoutInflater.from(parent.context)
- 在
onCreateViewHolder()
中,通过请求layoutinflater
膨胀它来创建view
。
传入视图的 XML 布局,以及视图的parent
视图组。第三个布尔值参数为attachToRoot
。此参数需为false
,因为RecyclerView
会在需要的时候为您将该项添加到视图层次结构中。
val view = layoutInflater
.inflate(R.layout.text_item_view, parent, false) as TextView
- 在
onCreateViewHolder()
中,返回使用view
创建的TextItemViewHolder
。
return TextItemViewHolder(view)
- 适配器需要在
data
发生更改时通知RecyclerView
,因为RecyclerView
对数据一无所知。它只会知道适配器为其提供的 ViewHolder。
如需在RecyclerView
显示的数据发生更改时告知RecyclerView
,请向SleepNightAdapter
类顶部的data
变量添加自定义 setter。在 setter 中,为data
指定一个新值,然后调用notifyDataSetChanged()
以触发使用新数据重新绘制列表。
var data = listOf<SleepNight>()
set(value) {
field = value
notifyDataSetChanged()
}
第 4 步:告知 RecyclerView 关于适配器的情况
RecyclerView
需要知道适配器用于获取 ViewHolder。
- 打开
SleepTrackerFragment.kt
。 - 在
onCreateview()
中,创建一个适配器。将以下代码放在ViewModel
模型创建代码之后,return
语句之前。
val adapter = SleepNightAdapter()
- 将
adapter
与RecyclerView
相关联。
binding.sleepList.adapter = adapter
- 清理并重建项目以更新
binding
对象。
如果您仍然看到binding.sleepList
或binding.FragmentSleepTrackerBinding
附近的错误,请使缓存失效并重启。(选择File > Invalidate Caches / Restart)。
如果您现在运行应用,不会发生错误,但当您点按开始,然后点按停止时,将不会显示任何数据。
第 5 步:将数据获取到适配器中
到目前为止,您已有适配器,以及将数据从适配器获取到 RecyclerView
中的方式。现在,您需要将 ViewModel
中的数据放入适配器。
- 打开
SleepTrackerViewModel
。 - 找到
nights
变量,该变量用于存储所有睡眠之夜的数据,也就是要显示的数据。对数据库调用getAllNights()
可设置nights
变量。 - 从
nights
中移除private
,因为您将创建一个需要访问此变量的观察器。您的声明应如下所示:
val nights = database.getAllNights()
- 在
database
软件包中,打开SleepDatabaseDao
。 - 找到
getAllNights()
函数。请注意,此函数将返回SleepNight
值的列表作为LiveData
。这意味着nights
变量包含由Room
持续更新的LiveData
,您可以观察nights
以了解其变化情况。 - 打开
SleepTrackerFragment
。 - 在
onCreateView()
中的adapter
创建下方,在nights
变量上创建一个观察器。
通过提供 fragment 的viewLifecycleOwner
作为生命周期所有者,您可以确保此观察器仅在RecyclerView
在屏幕上时才处于活动状态。
sleepTrackerViewModel.nights.observe(viewLifecycleOwner, Observer {
})
- 在观察器内,每当您获得非 null 值(针对
nights
)时,请将该值分配给适配器的data
。以下是观察器和数据设置的完整代码:
sleepTrackerViewModel.nights.observe(viewLifecycleOwner, Observer {
it?.let {
adapter.data = it
}
})
- 构建并运行代码。
如果您的适配器正常运行,您会看到一个列表,其中包含睡眠质量数据。点按开始后,左侧的屏幕截图会显示 -1。右侧的屏幕截图显示了点按停止并选择质量评分后更新后的睡眠质量数字。
第 6 步:探索 ViewHolder 的回收方式
RecyclerView
会回收 ViewHolder,这意味着前者会重复使用后者。当视图滚动出屏幕时,RecyclerView
会重复使用将要滚动到屏幕上的视图。
由于这些 ViewHolder 会被回收,因此请确保 onBindViewHolder()
设置或重置之前的项可能已对 ViewHolder 设置的所有自定义内容。
例如,在保持质量评分小于或等于 1 表示睡眠不佳的视图持有者中,您可以将文本颜色设置为红色。
- 在
SleepNightAdapter
类中的onBindViewHolder()
末尾添加以下代码。
if (item.sleepQuality <= 1) {
holder.textView.setTextColor(Color.RED) // red
}
- 运行应用。
- 添加一些较低的睡眠质量数据,并且数字为红色。
- 为睡眠质量添加较高的评分,直到您在屏幕上看到红色的高值。
由于RecyclerView
会重复使用 ViewHolder,因此它最终会重复使用某个红色 ViewHolder 来实现高质量评分。较高的评分会错误地显示为红色。
- 为解决此问题,请添加
else
语句,在质量不小于 1 的情况下将颜色设为黑色。
在这两种情况下,视图持有者会为每一项使用正确的文本颜色。
if (item.sleepQuality <= 1) {
holder.textView.setTextColor(Color.RED) // red
} else {
// reset
holder.textView.setTextColor(Color.BLACK) // black
}
- 运行应用,数字应始终具有正确的颜色。
恭喜!您现在已拥有功能齐全的基本 RecyclerView
。
在此任务中,您需要将简单的 ViewHolder 替换成可显示更多夜间睡眠数据的设备。
您添加到 Util.kt
的简单 ViewHolder
仅将 TextView
封装在 TextItemViewHolder
中。
class TextItemViewHolder(val textView: TextView): RecyclerView.ViewHolder(textView)
那么,RecyclerView
为什么不直接使用 TextView
呢?这一行代码提供了大量功能。ViewHolder
描述了项视图,以及关于其在 RecyclerView
中的位置的元数据。RecyclerView
依赖于此功能在列表滚动时正确放置视图,以及执行一些有趣的操作,例如在 Adapter
中添加或移除项时为视图添加动画效果。
如果 RecyclerView
确实需要访问存储在 ViewHolder
中的视图,可以使用视图持有者的 itemView
属性执行此操作。RecyclerView
在绑定项目以在屏幕上显示时,在视图周围绘制视图(如边框)时以及用于实现无障碍功能时使用 itemView
。
第 1 步:创建项布局
在此步骤中,您将为项创建布局文件。布局由一个 ConstraintLayout
组成,其中包含一个用于保存睡眠质量的 ImageView
、一个用于保存睡眠时长的 TextView
,以及一个用于以文本形式保存睡眠质量的 TextView
。因为您之前创建过布局,所以请复制并粘贴提供的 XML 代码。
- 创建新的布局资源文件,并将其命名为
list_item_sleep_night
。 - 将该文件中的所有代码替换为以下代码。然后,自行熟悉您刚刚创建的布局。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/quality_image"
android:layout_width="@dimen/icon_size"
android:layout_height="60dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@drawable/ic_sleep_5" />
<TextView
android:id="@+id/sleep_length"
android:layout_width="0dp"
android:layout_height="20dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/quality_image"
app:layout_constraintTop_toTopOf="@+id/quality_image"
tools:text="Wednesday" />
<TextView
android:id="@+id/quality_string"
android:layout_width="0dp"
android:layout_height="20dp"
android:layout_marginTop="8dp"
app:layout_constraintEnd_toEndOf="@+id/sleep_length"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/sleep_length"
app:layout_constraintTop_toBottomOf="@+id/sleep_length"
tools:text="Excellent!!!" />
</androidx.constraintlayout.widget.ConstraintLayout>
- 切换到 Android Studio 中的 Design 标签页。在设计视图中,您的布局如下面的屏幕截图所示。在蓝图视图中,它看起来如右侧的屏幕截图。
第 2 步:创建 ViewHolder
- 打开
SleepNightAdapter.kt
。 - 在
SleepNightAdapter
中创建一个名为ViewHolder
的类,并使其扩展RecyclerView.ViewHolder
。
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){}
- 在
ViewHolder
内,获取对视图的引用。您需要引用此ViewHolder
将要更新的视图。每次绑定此ViewHolder
时,您都需要访问这张图片和这两个文本视图。(稍后您将转换此代码以使用数据绑定。)
val sleepLength: TextView = itemView.findViewById(R.id.sleep_length)
val quality: TextView = itemView.findViewById(R.id.quality_string)
val qualityImage: ImageView = itemView.findViewById(R.id.quality_image)
第 3 步:在 SleepNightAdapter 中使用 ViewHolder
- 在
SleepNightAdapter
定义中,使用您刚刚创建的SleepNightAdapter.ViewHolder
,而不是TextItemViewHolder
。
class SleepNightAdapter: RecyclerView.Adapter<SleepNightAdapter.ViewHolder>() {
更新 onCreateViewHolder()
:
- 更改
onCreateViewHolder()
的签名以返回ViewHolder
。 - 更改布局膨胀器以使用正确的布局资源
list_item_sleep_night
。 - 移除到
TextView
的类型转换。 - 请返回
ViewHolder
,而不是返回TextItemViewHolder
。
以下是更新后的onCreateViewHolder()
函数:
override fun onCreateViewHolder(
parent: ViewGroup, viewType: Int): ViewHolder {
val layoutInflater =
LayoutInflater.from(parent.context)
val view = layoutInflater
.inflate(R.layout.list_item_sleep_night,
parent, false)
return ViewHolder(view)
}
更新 onBindViewHolder()
:
- 更改
onBindViewHolder()
的签名,使holder
参数为ViewHolder
,而不是TextItemViewHolder
。 - 在
onBindViewHolder()
中,删除item
定义以外的所有代码。 - 定义一个
val
res
,用于存储对此视图的resources
的引用。
val res = holder.itemView.context.resources
- 将
sleepLength
文本视图的文本设置为时长。复制以下代码,它用于调用随起始代码提供的格式设置函数。
holder.sleepLength.text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, res)
- 这会显示错误,因为需要定义
convertDurationToFormatted()
。打开Util.kt
并取消注释代码和关联的导入内容。(依次选择 Code > Comment with Line Comment。) - 返回
onBindViewHolder()
,使用convertNumericQualityToString()
设置画质。
holder.quality.text= convertNumericQualityToString(item.sleepQuality, res)
- 您可能需要手动导入这些函数。
import com.example.android.trackmysleepquality.convertDurationToFormatted
import com.example.android.trackmysleepquality.convertNumericQualityToString
- 请为画质设置正确的图标。起始代码中为您提供了新的
ic_sleep_active
图标。
holder.qualityImage.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
})
- 下面是更新后的
onBindViewHolder()
函数,用于设置ViewHolder
的所有数据:
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = data[position]
val res = holder.itemView.context.resources
holder.sleepLength.text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, res)
holder.quality.text= convertNumericQualityToString(item.sleepQuality, res)
holder.qualityImage.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
})
}
- 运行应用。您的显示屏应如以下屏幕截图所示,显示睡眠质量图标,以及睡眠时长和睡眠质量的文本。
您的 RecyclerView
现已完成!您学习了如何实现 Adapter
和 ViewHolder
,以及如何借助 RecyclerView
Adapter
将它们结合使用来显示列表。
到目前为止,您的代码显示了创建适配器和 ViewHolder 的过程。不过,您可以改进此代码。要显示的代码和用于管理 ViewHolder 的代码混合在一起,并且 onBindViewHolder()
知道如何更新 ViewHolder
的详细信息。
在正式版应用中,您可能有多个 ViewHolder、更复杂的适配器,还有多个开发者在进行更改。您应构建代码,使与 ViewHolder 相关的所有内容仅在 ViewHolder 中。
第 1 步:重构 onBindViewHolder()
在此步骤中,您将重构代码,并将所有 ViewHolder 功能移至 ViewHolder
内。此重构的目的不是改变应用在用户眼中的外观,而是让开发者更轻松、更安全地编写代码。幸运的是,Android Studio 提供了可提供帮助的工具。
- 在
SleepNightAdapter
的onBindViewHolder()
中,选择除语句之外的所有内容以声明变量item
。 - 右键点击,然后依次选择 Refactor > Extract > Function。
- 将该函数命名为
bind
,并接受建议的参数。点击 OK。bind()
函数位于onBindViewHolder()
下方。
private fun bind(holder: ViewHolder, item: SleepNight) {
val res = holder.itemView.context.resources
holder.sleepLength.text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, res)
holder.quality.text = convertNumericQualityToString(item.sleepQuality, res)
holder.qualityImage.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
})
}
- 将光标放在
bind()
的holder
参数的holder
一词上。按Alt+Enter
(在 Mac 上,按Option+Enter
)打开 intent 菜单。选择 Convert parameter to 接收器 以将其转换为具有以下签名的扩展函数:
private fun ViewHolder.bind(item: SleepNight) {...}
- 将
bind()
函数剪切并粘贴到ViewHolder
中。 - 将
bind()
设为公开。 - 如有必要,将
bind()
导入适配器。 - 由于它现在位于
ViewHolder
中,因此您可以移除签名的ViewHolder
部分。下面是ViewHolder
类中bind()
函数的最终代码。
fun bind(item: SleepNight) {
val res = itemView.context.resources
sleepLength.text = convertDurationToFormatted(
item.startTimeMilli, item.endTimeMilli, res)
quality.text = convertNumericQualityToString(
item.sleepQuality, res)
qualityImage.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 步:重构 onCreateViewHolder
适配器中的 onCreateViewHolder()
方法目前会通过 ViewHolder
的布局资源膨胀视图。但是,膨胀与适配器无关,只与 ViewHolder
相关。膨胀应在 ViewHolder
中进行。
- 在
onCreateViewHolder()
中,选择函数正文中的所有代码。 - 右键点击,然后依次选择 Refactor > Extract > Function。
- 将该函数命名为
from
,并接受建议的参数。点击 OK。 - 将光标放在函数名称
from
上。按Alt+Enter
(在 Mac 上,按Option+Enter
)打开 intent 菜单。 - 选择 Move to companion object。
from()
函数必须位于伴生对象中,以便可以在ViewHolder
类中调用,而不是在ViewHolder
实例上调用。 - 将
companion
对象移至ViewHolder
类。 - 将
from()
设为公开。 - 在
onCreateViewHolder()
中,更改return
语句以返回在ViewHolder
类中调用from()
的结果。
您已完成的onCreateViewHolder()
和from()
方法应如以下代码所示,而且您的代码应正常构建和运行。
override fun onCreateViewHolder(parent: ViewGroup, viewType:
Int): ViewHolder {
return ViewHolder.from(parent)
}
companion object {
fun from(parent: ViewGroup): ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val view = layoutInflater
.inflate(R.layout.list_item_sleep_night, parent, false)
return ViewHolder(view)
}
}
- 更改
ViewHolder
类的签名,以便将构造函数设为私有。由于from()
现在是用于返回新ViewHolder
实例的方法,因此任何人都没有理由再调用ViewHolder
的构造函数。
class ViewHolder private constructor(itemView: View) : RecyclerView.ViewHolder(itemView){
- 运行应用。您的应用应该像以前一样顺利构建和运行,这是重构后的理想结果。
Android Studio 项目:RecyclerViewFundamentals
- 显示数据列表或数据网格是 Android 中最常见的界面任务之一。即使显示超大列表,
RecyclerView
也非常高效。 RecyclerView
只会处理或绘制当前显示在屏幕上的项。- 当某个项滚动出屏幕时,RecyclerView 会回收其视图。也就是说,这个项中会填充滚动到屏幕上的新内容。
- 软件工程中的适配器模式有助于对象与其他 API 配合使用。
RecyclerView
使用适配器将应用数据转换为它可以显示的内容,而无需更改应用存储和处理相应数据的方式。
如需在 RecyclerView
中显示您的数据,您需要以下几个部分:
- RecyclerView
如需创建RecyclerView
的实例,请在布局文件中定义<RecyclerView>
元素。 - LayoutManager
RecyclerView
使用LayoutManager
来组织RecyclerView
中项的布局,例如布局在网格或线性列表中。
在布局文件的<RecyclerView>
中,将app:layoutManager
属性设置为布局管理器(如LinearLayoutManager
或GridLayoutManager
)。
您也可以通过编程方式为RecyclerView
设置LayoutManager
。(此 Codelab 将在以后的 Codelab 中介绍。) - 为每项内容指定布局
在 XML 布局文件中为一项数据项创建布局。 - Adapter
创建一个适配器,用于准备数据以及数据在ViewHolder
中的显示方式。将适配器与RecyclerView
相关联。
运行RecyclerView
时,会使用 Adapter 确定如何在屏幕上显示数据。
该适配器要求您实现以下方法:
–getItemCount()
返回项数。
–onCreateViewHolder()
返回列表中相应项的ViewHolder
。
–onBindViewHolder()
使数据适应列表中项的视图。 - ViewHolder
ViewHolder
包含用于显示内容布局中的一项内容的视图信息。 - 适配器中的
onBindViewHolder()
方法会根据视图调整数据。应始终替换此方法。通常,onBindViewHolder()
会膨胀项目的布局,并将数据放入布局的视图中。 - 由于
RecyclerView
对数据一无所知,因此Adapter
需要在数据发生变化时通知RecyclerView
。使用notifyDataSetChanged()
通知Adapter
数据已更改。
Udacity 课程:
Android 开发者文档:
此部分列出了在由讲师主导的课程中,学生学习此 Codelab 后可能需要完成的家庭作业。讲师自行决定是否执行以下操作:
- 根据需要布置作业。
- 告知学生如何提交家庭作业。
- 给家庭作业评分。
讲师可以酌情采纳这些建议,并且可以自由布置自己认为合适的任何其他家庭作业。
如果您是在自学此 Codelab,可随时通过这些家庭作业来检测您的知识掌握情况。
回答以下问题
问题 1
RecyclerView
是如何显示项的?请选择所有适用的选项。
▢ 以列表或网格形式显示各项。
▢ 垂直或水平滚动。
▢ 在较大的设备(例如平板电脑)上沿对角线滚动。
▢ 允许在列表或网格无法满足用例要求时使用自定义布局。
问题 2
使用 RecyclerView
有哪些优势?请选择所有适用的选项。
▢ 高效地显示大型列表。
▢ 自动更新数据。
▢ 最大限度地减少在更新或删除列表中的项或者向列表添加项时进行刷新的需求。
▢ 重复使用滚动出屏幕的视图来显示滚动到屏幕上的下一项。
问题 3
使用适配器的原因有哪些?请选择所有适用的选项。
▢ 分离关注点有助于更轻松地更改和测试代码。
▢ RecyclerView
与显示的数据无关。
▢ 数据处理层无需关注数据的显示方式。
▢ 应用运行速度会更快。
问题 4
以下关于 ViewHolder
的说法中,哪些是正确的?请选择所有适用的选项。
▢ ViewHolder
布局是在 XML 布局文件中定义的。
▢ 数据集内的每个数据单元都有一个 ViewHolder
。
▢ 一个 RecyclerView
中可以有多个 ViewHolder
。
▢ Adapter
可以将数据绑定到 ViewHolder
。