Android Kotlin 基础知识:RecyclerView 基础知识

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

简介

此 Codelab 会教您如何使用 RecyclerView 显示项列表。之前的一系列 Codelab 中介绍了一款睡眠跟踪器应用;您将以在其中学到的概念为基础,了解一种更好、更通用的数据显示方式。您的新应用将更新这个睡眠跟踪器,让它使用带有推荐架构的 RecyclerView

您应当已掌握的内容

您应熟悉以下内容:

  • 使用 activity、fragment 和视图构建基本界面 (UI)。
  • 在 fragment 之间导航,并使用 safeArgs 在 fragment 之间传递数据。
  • 使用视图模型、转换以及 LiveData 及其观察器。
  • 创建 Room 数据库、创建 DAO 以及定义实体。
  • 将协程用于数据库任务和其他长时间运行的任务。

学习内容

  • 如何将 RecyclerViewAdapterViewHolder 结合使用来显示项列表。

实践内容

  • 将上一课中的 TrackMySleepQuality 应用改为使用 RecyclerView 来显示睡眠质量数据。

在此 Codelab 中,您将为一款用于跟踪睡眠质量的应用构建 RecyclerView 部分。该应用使用 Room 来存储一段时间的睡眠数据。

起始睡眠跟踪器应用有两个屏幕,以 fragment 表示,如下图所示。

e28eb795b6812ee4.png

左侧所示的第一个屏幕包含用于开始和停止跟踪的按钮。这个屏幕还会显示用户的所有睡眠数据。CLEAR 按钮用于永久删除应用针对用户收集的所有数据。右侧所示的第二个屏幕用于选择睡眠质量评分。

此应用采用简化的架构,其中包括一个界面控制器、ViewModelLiveData。此应用还使用 Room 数据库永久保存睡眠数据。

49f975f1e5fe689.png

第一个屏幕上显示的睡眠之夜列表功能正常,但并不美观。该应用使用复杂的格式设置工具为文本视图创建文本字符串,并为质量创建数值。此外,这种设计有点复杂,会降低我们的可伸缩性。在此 Codelab 中解决所有这些问题后,最终应用的功能将与原始应用相同,但改进后的主屏幕会更易读:

76d78f63f88c3c86.png

显示数据列表或数据网格是 Android 中最常见的界面任务之一。列表包含从简单到非常复杂的各种内容。文本视图列表可以显示简单的数据,例如购物清单。复杂列表(例如带注解的度假目的地列表)可以在带标题的滚动网格内向用户显示许多详细信息。

为了支持所有这些用例,Android 提供了 RecyclerView widget。

643a2240444361ad.png

RecyclerView 的最大优势在于,它对大型列表来说非常高效:

  • 默认情况下,RecyclerView 仅会处理或绘制当前显示在屏幕上的项。例如,如果您的列表包含一千个元素,但只有 10 个元素可见,那么 RecyclerView 仅会完成在屏幕上绘制这 10 个项的工作。当用户滚动时,RecyclerView 会确定应在屏幕上显示哪些新项,然后仅完成显示这些项的工作。
  • 当某个项滚动出屏幕时,RecyclerView 会回收其视图。也就是说,这个项中会填充滚动到屏幕上的新内容。RecyclerView 的这种行为可以节省大量处理时间,并能让列表顺畅地滚动。
  • 当某个项发生变化时,RecyclerView 无需重新绘制整个列表即可更新该项。在显示包含复杂项的长列表时,这可以极大地提高效率!

在下图显示的序列中,可以看到一个填充了数据 ABC 的视图。当该视图滚动出屏幕之后,RecyclerView 会重复使用该视图来显示新数据 XYZ

dcf4599789b9c2a1.png

适配器模式

如果您曾在使用不同电源插座的国家/地区间旅行,那么您可能知道如何使用适配器将设备插入国外的插座。通过使用适配器,您可将一种插头转换为另一种插头,这实际上是将一个接口转换为另一个接口。

软件工程中的适配器模式使用了类似的概念。此模式允许将一个类的 API 用作另一个 API。RecyclerView 使用适配器将应用数据转换为 RecyclerView 可以显示的内容,而无需更改应用存储和处理相应数据的方式。对于睡眠跟踪器应用,您需要构建一个适配器,用于将 Room 数据库中的数据调整为 RecyclerView 知道如何显示的内容,而无需更改 ViewModel

实现 RecyclerView

4e9c18b463f00bf7.png

如需在 RecyclerView 中显示您的数据,您需要以下几个部分:

  • 要显示的数据。
  • 在布局文件中定义的一个 RecyclerView 实例,用作视图的容器。
  • 一个数据项的布局。如果所有列表项看起来都一样,您可以对所有这些列表项使用相同的布局,但这不是强制性要求。项布局必须与 fragment 的布局分开创建,以便一次创建一个项视图,并在其中填充数据。
  • 一个布局管理器。布局管理器负责处理视图中界面组件的组织(布局)。
  • 一个 ViewHolder。该 ViewHolder 用于扩展 ViewHolder 类。它包含视图信息,用于显示项布局中的一项。ViewHolder 还会添加一些信息,供 RecyclerView 用于在屏幕上高效移动视图。
  • 一个适配器。该适配器可将您的数据与 RecyclerView 相关联。它会调整数据,使其可在 ViewHolder 中显示。RecyclerView 会使用适配器确定如何在屏幕上显示数据。

在此任务中,您将向布局文件添加 RecyclerView,并设置 Adapter 以向 RecyclerView 公开睡眠数据。

第 1 步:使用 LayoutManager 添加 RecyclerView

在此步骤中,您需要将 fragment_sleep_tracker.xml 文件中的 ScrollView 替换为 RecyclerView

  1. 从 GitHub 下载 RecyclerViewFundamentals-Starter 应用。
  2. 构建并运行应用。请注意数据如何显示为简单文本。
  3. 在 Android Studio 的 Design 标签页中,打开 fragment_sleep_tracker.xml 布局文件。
  4. Component Tree 窗格中,删除 ScrollView。此操作还会删除 ScrollView 内的 TextView
  5. Palette 窗格中,滚动浏览左侧的组件类型列表,找到 Containers,然后选择它。
  6. RecyclerViewPalette 窗格拖动到 Component Tree 窗格中。将 RecyclerView 放入 ConstraintLayout 中。

8c6cfd99d4237c7d.png

  1. 如果屏幕上打开一个对话框,询问您是否要添加依赖项,请点击 OK,让 Android Studio 将 recyclerview 依赖项添加到您的 Gradle 文件中。此过程可能需要几秒钟的时间,然后您的应用会同步。

828133c3a2314dc7.png

  1. 打开模块 build.gradle 文件,滚动到末尾,并记下新的依赖项,它类似于以下代码:
implementation 'androidx.recyclerview:recyclerview:1.0.0'
  1. 切换回 fragment_sleep_tracker.xml
  2. Code 标签页中,查找如下所示的 RecyclerView 代码:
<androidx.recyclerview.widget.RecyclerView
   android:layout_width="match_parent"
   android:layout_height="match_parent" />
  1. RecyclerView 提供 sleep_listid
android:id="@+id/sleep_list"
  1. 放置 RecyclerView,使其占据屏幕上 ConstraintLayout 内的剩余部分。为此,请将 RecyclerView 的顶部约束到 START 按钮,将底部约束到 CLEAR 按钮,并将两侧约束到相应父级。在布局编辑器或 XML 中,使用以下代码将布局宽度和高度设置为 0dp:
    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"
  1. RecyclerView XML 添加一个布局管理器。每个 RecyclerView 都需要一个布局管理器,用于指示如何在列表中放置项。Android 提供了一个 LinearLayoutManager,默认情况下,它会在一个包含全宽行的垂直列表中排列各项。
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
  1. 切换到 Design 标签页,请注意,添加的约束条件已导致 RecyclerView 展开,从而填充可用空间。

eef3940d35065b97.png

第 2 步:创建列表项布局和文本 ViewHolder

RecyclerView 只是一个容器。在此步骤中,您将为要在 RecyclerView 中显示的项创建布局和基础架构。

为了尽快获得一个能正常运行的 RecyclerView,请先使用仅以数值形式显示睡眠质量的简化列表项。为此,您需要一个 ViewHolder - TextItemViewHolder。您还需要一个数据视图 - TextView。(在稍后的步骤中,您将详细了解 ViewHolder 以及如何排列所有睡眠数据。)

  1. 创建一个名为 text_item_view.xml 的布局文件。用什么充当根元素并不重要,因为您会替换模板代码。
  2. text_item_view.xml 中,删除所有指定代码。
  3. 添加 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" />
  1. 打开 Util.kt。滚动到末尾,添加如下所示的定义,这将创建 TextItemViewHolder 类。将代码放在该文件底部最后一个英文大括号之后。之所以将代码置于 Util.kt 中,是因为此 ViewHolder 是临时的,您稍后要替换它。
class TextItemViewHolder(val textView: TextView): RecyclerView.ViewHolder(textView)
  1. 如果系统提示,请导入 android.widget.TextViewandroidx.recyclerview.widget.RecyclerView

第 3 步:创建 SleepNightAdapter

实现 RecyclerView 时,核心任务就是创建适配器。

您的项视图有一个简单的 ViewHolder,并且每个项都有一个布局。完成这两项操作后,您现在就可以创建适配器了。适配器会创建一个ViewHolder,并在其中填充数据以供 RecyclerView 显示。

  1. sleeptracker 软件包中,创建一个名为 SleepNightAdapter 的新 Kotlin 类。
  2. SleepNightAdapter 类扩展 RecyclerView.Adapter。这个类称为 SleepNightAdapter,因为它会将 SleepNight 对象调整为 RecyclerView 可以使用的东西。适配器需要知道要使用哪个 ViewHolder,因此请传入 TextItemViewHolder。在看到提示时导入必要的组件,然后您会看到错误消息,因为存在要实现的强制性方法。
class SleepNightAdapter: RecyclerView.Adapter<TextItemViewHolder>() {}
  1. SleepNightAdapter 的顶层,创建一个 listOf SleepNight 变量来保存数据。
var data =  listOf<SleepNight>()
  1. SleepNightAdapter 中,替换 getItemCount() 以返回 data 中睡眠之夜列表的大小。RecyclerView 需要知道适配器有多少个项可供其显示,为此,它会调用 getItemCount()
override fun getItemCount() = data.size
  1. SleepNightAdapter 中,替换 onBindViewHolder() 函数,如下所示。

RecyclerView 会调用 onBindViewHolder() 函数,以在指定位置显示一个列表项的数据。因此,onBindViewHolder() 方法采用两个参数:ViewHolder,以及要绑定数据的位置。对于此应用,ViewHolder 是 TextItemViewHolder,位置是列表中的位置。

override fun onBindViewHolder(holder: TextItemViewHolder, position: Int) {
}
  1. onBindViewHolder() 中,为数据中位于指定位置的一个项创建一个变量。
 val item = data[position]
  1. 您刚刚创建的 ViewHolder 具有一个名为 textView 的属性。在 onBindViewHolder() 中,将 textViewtext 设置为睡眠质量数值。下面这段代码只显示了数值列表,不过您可以借助这个简单示例了解适配器如何将数据放入 ViewHolder 和屏幕中。
holder.textView.text = item.sleepQuality.toString()
  1. SleepNightAdapter 中,替换并实现 onCreateViewHolder(),后者会在 RecyclerView 需要 ViewHolder 时进行调用。

此函数采用两个参数,并返回 ViewHolderparent 参数(即用于容纳 ViewHolder 的视图组)始终为 RecyclerView。当同一 RecyclerView 中有多个视图时,就会用到 viewType 参数。例如,如果您将包含多个文本视图、一张图片和一个视频的列表都放在同一个 RecyclerView 中,那么 onCreateViewHolder() 函数需要知道要使用哪种视图类型。

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TextItemViewHolder {
}
  1. onCreateViewHolder() 中,创建 LayoutInflater 的实例。

布局膨胀器知道如何通过 XML 布局创建视图。context 包含有关如何正确膨胀视图的信息。在 recycler 视图的适配器中,请始终传入 parent 视图组的上下文,即 RecyclerView

val layoutInflater = LayoutInflater.from(parent.context)
  1. onCreateViewHolder() 中,创建 view,方法是让 layoutinflater 膨胀它。

传入视图的 XML 布局和 parent 视图组。第三个布尔值参数是 attachToRoot。此参数需为 false,因为 RecyclerView 会在需要的时候替您将此项添加到视图层次结构中。

val view = layoutInflater
       .inflate(R.layout.text_item_view, parent, false) as TextView
  1. onCreateViewHolder() 中,返回使用 view 创建的 TextItemViewHolder
return TextItemViewHolder(view)
  1. 适配器需要在 data 发生更改时通知 RecyclerView,因为 RecyclerView 对数据一无所知。它只了解适配器向其提供的 ViewHolder。

为了告知 RecyclerView 其显示的数据已发生更改,请将自定义 setter 添加到位于 SleepNightAdapter 类顶部的 data 变量中。在 setter 中,为 data 提供一个新值,然后调用 notifyDataSetChanged() 以触发使用新数据重新绘制列表的操作。

var data =  listOf<SleepNight>()
   set(value) {
       field = value
       notifyDataSetChanged()
   }

第 4 步:告知 RecyclerView 关于适配器的情况

RecyclerView 需要知道用于获取 ViewHolder 的适配器的情况。

  1. 打开 SleepTrackerFragment.kt
  2. onCreateview() 中,创建一个适配器。将以下代码放在 ViewModel 模型创建代码之后,return 语句之前。
val adapter = SleepNightAdapter()
  1. 获取对绑定对象的引用后,将 adapterRecyclerView 关联。
binding.sleepList.adapter = adapter
  1. 清理并重建项目以更新 binding 对象。

如果您仍在 binding.sleepListbinding.FragmentSleepTrackerBinding 附近看到错误,请使缓存失效并重启。(依次选择 File > Invalidate Caches/Restart。)

如果您现在运行应用,它不会出现任何错误;但依次点按 STARTSTOP 后,应用不会显示任何数据。

第 5 步:将数据获取到适配器中

到目前为止,您已有适配器,以及将数据从适配器获取到 RecyclerView 中的方式。现在,您需要将数据从 ViewModel 获取到适配器中。

  1. 打开 SleepTrackerViewModel
  2. 找到 nights 变量,该变量用于存储所有睡眠之夜的数据,也就是要显示的数据。可通过对数据库调用 getAllNights() 来设置 nights 变量。
  3. nights 中移除 private,因为您将创建一个需要访问此变量的观察器。您的声明应如下所示:
val nights = database.getAllNights()
  1. database 软件包中,打开 SleepDatabaseDao
  2. 找到 getAllNights() 函数。请注意,此函数会将 SleepNight 值列表作为 LiveData 返回。这意味着 nights 变量包含由 Room 持续更新的 LiveData,您可以观察 nights 以了解其变化情况。
  3. 打开 SleepTrackerFragment
  4. onCreateView() 中,在实例化 ViewModel 并拥有其引用后,针对 nights 变量创建一个观察器。

通过提供 fragment 的 viewLifecycleOwner 作为生命周期所有者,您可以确保此观察器仅当 RecyclerView 位于屏幕上时才处于活动状态。

sleepTrackerViewModel.nights.observe(viewLifecycleOwner, Observer {
   })
  1. 在观察器内,每当您获得非 null 值(针对 nights)时,请将该值分配给适配器的 data。以下是观察器和数据设置的完整代码:
sleepTrackerViewModel.nights.observe(viewLifecycleOwner, Observer {
   it?.let {
       adapter.data = it
   }
})
  1. 构建并运行您的代码。

如果适配器正常运行,您会看到以列表形式显示的睡眠质量数值。点按 START 后,左侧的屏幕截图会显示 -1。点按 STOP 并选择一个质量评分后,右侧的屏幕截图将显示更新后的睡眠质量数值。

89f71f10deda270.png

第 6 步:探索 ViewHolder 的回收方式

RecyclerView 会回收 ViewHolder,这意味着前者会重复使用后者。当某个视图滚动出屏幕之后,RecyclerView 会重复使用该视图来显示将要滚动到屏幕上的项。

由于这些 ViewHolder 会被回收,因此请确保 onBindViewHolder() 设置或重置之前的项可能已对 ViewHolder 设置的所有自定义内容。

例如,您可能会将用于保存小于或等于 1 的质量评分且代表睡眠不佳的 ViewHolder 的文本颜色设置为红色。

  1. SleepNightAdapter 类中,在 onBindViewHolder() 的末尾,添加以下代码。
if (item.sleepQuality <= 1) {
   holder.textView.setTextColor(Color.RED) // red
}
  1. 运行应用。
  2. 添加一些表示睡眠质量较低的数据,相应数值将为红色。
  3. 添加一些表示睡眠质量较高的评分,直到您在屏幕上看到红色的高评分数值。

RecyclerView 会重复使用 ViewHolder,因此最终会将其中一个红色的 ViewHolder 重复用于高质量评分。高评分被错误地显示为了红色。

b57e7915d6dd3c78.png

  1. 为解决此问题,请添加 else 语句,以便在质量评分高于 1 的情况下将相应颜色设置为黑色。

在两个条件都明确的情况下,ViewHolder 将为每个项使用正确的文本颜色。

if (item.sleepQuality <= 1) {
   holder.textView.setTextColor(Color.RED) // red
} else {
   // reset
   holder.textView.setTextColor(Color.BLACK) // black
}
  1. 运行应用,数值应始终具有正确的颜色。

恭喜!您现在已拥有功能齐全的基本 RecyclerView

在此任务中,您需要将简单的 ViewHolder 替换成可显示更多夜间睡眠数据的设备。

您添加到 Util.kt 的简单 ViewHolder 仅会将 TextView 封装在 TextItemViewHolder 中。

class TextItemViewHolder(val textView: TextView): RecyclerView.ViewHolder(textView)

那么,RecyclerView 为什么不直接使用 TextView 呢?这一行代码提供了大量功能。ViewHolder 描述了项视图,以及关于其在 RecyclerView 中的位置的元数据。RecyclerView 依赖于此功能在列表滚动时正确放置视图,以及执行一些有趣的操作,例如在 Adapter 中添加或移除项时为视图添加动画效果。

如果 RecyclerView 确实需要访问 ViewHolder 中存储的视图,可以使用 ViewHolder 的 itemView 属性执行此操作。RecyclerView 会在以下情况下使用 itemView:当它绑定某个项以将其显示在屏幕上时;在视图周围绘制边框等装饰时;以及实现无障碍功能时。

第 1 步:创建项布局

在此步骤中,您将为项创建布局文件。布局由一个 ConstraintLayout 组成,其中包含一个用于保存睡眠质量的 ImageView、一个用于保存睡眠时长的 TextView,以及一个用于以文本形式保存睡眠质量的 TextView。因为您之前创建过布局,所以请复制并粘贴提供的 XML 代码。

  1. 创建新的布局资源文件,并将其命名为 list_item_sleep_night
  2. 将该文件中的所有代码替换为以下代码。然后,自行熟悉您刚刚创建的布局。
<?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>
  1. 切换到 Android Studio 中的 Design 标签页。在 Design 视图中,您的布局将如下方左侧的屏幕截图所示。在 Blueprint 视图中,它看起来如右侧的屏幕截图所示。

8240174f46c2c380.png

第 2 步:创建 ViewHolder

  1. 打开 SleepNightAdapter.kt
  2. SleepNightAdapter 中创建一个名为 ViewHolder 的类,并使其扩展 RecyclerView.ViewHolder
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){}
  1. 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

  1. SleepNightAdapter 类签名定义中,使用您刚刚创建的 SleepNightAdapter.ViewHolder,而不是 TextItemViewHolder
class SleepNightAdapter: RecyclerView.Adapter<SleepNightAdapter.ViewHolder>() {

更新 onCreateViewHolder()

  1. 更改 onCreateViewHolder() 的签名以返回 ViewHolder
  2. 更改布局膨胀器以使用正确的布局资源 list_item_sleep_night
  3. 移除到 TextView 的类型转换。
  4. 返回 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()

  1. 更改 onBindViewHolder() 的签名,使 holder 参数为 ViewHolder,而不是 TextItemViewHolder
  2. onBindViewHolder() 中,删除除 item 定义之外的所有代码。
  3. 定义一个 val res,用于保存对此视图的 resources 的引用。
val res = holder.itemView.context.resources
  1. sleepLength 文本视图的文本设置为时长。复制以下代码,它用于调用随起始代码提供的格式设置函数。
holder.sleepLength.text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, res)
  1. 这会显示错误,因为需要定义 convertDurationToFormatted()。打开 Util.kt 并取消注释代码和关联的导入内容。(依次选择 Code > Comment with Line Comment。)
  2. 返回到 SleepNightAdapter.kt 中的 onBindViewHolder(),使用 convertNumericQualityToString() 设置质量。
holder.quality.text= convertNumericQualityToString(item.sleepQuality, res)
  1. 您可能需要手动导入这些函数。
import com.example.android.trackmysleepquality.convertDurationToFormatted
import com.example.android.trackmysleepquality.convertNumericQualityToString
  1. 设置质量后,立即为质量设置正确的图标。起始代码中为您提供了新的 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
})
  1. 下面是更新且完成后的 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
        })
    }
  1. 运行应用。您的屏幕应如以下屏幕截图所示,显示睡眠质量图标,以及表示睡眠时长和睡眠质量的文本。

d5deef86fa39fbfc.png

您的 RecyclerView 现已完成!您学习了如何实现 AdapterViewHolder,以及如何借助 RecyclerView Adapter 将它们结合使用来显示列表。

到目前为止,您的代码显示了创建适配器和 ViewHolder 的过程。不过,您可以改进此代码。要显示的代码和用于管理 ViewHolder 的代码混合在一起,并且 onBindViewHolder() 知道如何更新 ViewHolder 的详细信息。

在正式版应用中,您可能有多个 ViewHolder、更复杂的适配器,还有多个开发者在进行更改。您应设置代码结构,使与 ViewHolder 相关的所有内容都仅位于 ViewHolder 中。

第 1 步:重构 onBindViewHolder()

在此步骤中,您将重构代码,并将所有 ViewHolder 功能移至 ViewHolder 内。此重构的目的不是改变应用在用户眼中的外观,而是让开发者更轻松、更安全地编写代码。幸运的是,Android Studio 提供了可提供帮助的工具。

  1. SleepNightAdapter.ktonBindViewHolder() 函数中,选择除用于声明变量 item 的语句之外的所有内容。
  2. 右键点击,然后依次选择 Refactor > Extract > Function
  3. 将该函数命名为 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
        })
    }
  1. 使用以下签名创建一个扩展函数
private fun ViewHolder.bind(item: SleepNight) {...}
  1. 剪切此 ViewHolder.bind() 函数,并粘贴到 SleepNightAdapter.kt 底部的 ViewHolder 内部类中。
  2. bind() 设为公开。
  3. 根据需要将 bind() 导入适配器中。
  4. 由于它现在位于 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
   })
}

现在,在 SleepNightAdapter.kt 中,onBindViewHolder() 函数在 bind(holder, item) 上显示了未解析的引用错误。此函数现在是 ViewHolder 内部类的一部分,因此我们需要指定特定的 ViewHolder。这也意味着我们可以移除第一个参数。

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
   val item = data[position]
   holder.bind(item)
}

第 2 步:重构 onCreateViewHolder

适配器中的 onCreateViewHolder() 方法目前会通过 ViewHolder 的布局资源膨胀视图。但是,膨胀与适配器无关,只与 ViewHolder 相关。膨胀应在 ViewHolder 中进行。

  1. onCreateViewHolder() 中,选择函数主体中的所有代码。
  2. 右键点击,然后依次选择 Refactor > Extract > Function
  3. 将该函数命名为 from,并接受建议的参数。点击 OK
  4. 将光标放在函数名称 from 上。按 Alt+Enter(在 Mac 上,按 Option+Enter)打开 intent 菜单。
  5. 选择 Move to companion objectfrom() 函数必须位于伴生对象中,以便对 ViewHolder 类调用,而不是对 ViewHolder 实例调用。
  6. companion 对象移入 ViewHolder 类 中。
  7. from() 设为公开。
  8. 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)
   }
}
  1. 更改 ViewHolder 类的签名,以便将构造函数设为私有。由于 from() 现在是用于返回新 ViewHolder 实例的方法,因此任何人都没有理由再调用 ViewHolder 的构造函数。
class ViewHolder private constructor(itemView: View) : RecyclerView.ViewHolder(itemView){
  1. 运行应用。您的应用应该像以前一样顺利构建和运行,这是重构后的理想结果。

Android Studio 项目:RecyclerViewFundamentals

  • 显示数据列表或数据网格是 Android 中最常见的界面任务之一。RecyclerView 旨在高效地显示列表,哪怕列表相当大也是如此。
  • RecyclerView 只会处理或绘制当前显示在屏幕上的项。
  • 当某个项滚动出屏幕时,RecyclerView 会回收其视图。也就是说,这个项中会填充滚动到屏幕上的新内容。
  • 软件工程中的适配器模式可帮助对象与其他 API 协同工作。RecyclerView 使用适配器将应用数据转换为它可以显示的内容,而无需更改应用存储和处理相应数据的方式。

如需在 RecyclerView 中显示您的数据,您需要以下几个部分:

  • RecyclerView:如需创建 RecyclerView 的实例,请在布局文件中定义 <RecyclerView> 元素。
  • LayoutManagerRecyclerView 使用 LayoutManager 组织 RecyclerView 中各项的布局,例如将其排列为网格或线性列表。

在布局文件的 <RecyclerView> 中,将 app:layoutManager 属性设置为布局管理器(例如 LinearLayoutManagerGridLayoutManager)。

您还可以通过程序化方式为 RecyclerView 设置 LayoutManager。(后续某个 Codelab 中将介绍此方法。)

  • 每个项的布局:在 XML 布局文件中为每个数据项创建一个布局。
  • 适配器:创建一个用于准备数据及其在 ViewHolder 中如何显示的适配器。将适配器与 RecyclerView 相关联。

RecyclerView 运行时,它会使用适配器确定如何在屏幕上显示数据。

适配器要求您实现以下方法:- getItemCount(),用于返回项的数量;- onCreateViewHolder(),用于返回列表中某个项的 ViewHolder;- onBindViewHolder(),用于根据列表中某个项的视图调整数据。

  • ViewHolderViewHolder 包含视图信息,用于显示项布局中的一项。
  • 适配器中的 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

开始学习下一课:DiffUtil 以及将数据绑定与 RecyclerView 结合使用