Android Kotlin 基础知识:与 RecyclerView 项进行交互

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

简介

大多数使用列表和网格来显示项的应用都允许用户与这些项进行交互。点按列表中的某个项并查看该项的详细信息,是这种交互的常见用例。为实现此目的,您可以添加点击监听器来响应用户点按项的操作。

在此 Codelab 中,您将向 RecyclerView 添加这种类型的交互,并在之前的一系列 Codelab 中的扩展版睡眠跟踪应用的基础上进行构建。

您应当已掌握的内容

  • 使用 activity、fragment 和视图构建基本界面。
  • 在 fragment 之间导航,并使用 safeArgs 在 fragment 之间传递数据。
  • 视图模型、视图模型工厂、转换以及 LiveData 及其观察器。
  • 如何创建 Room 数据库、创建数据访问对象 (DAO) 以及定义实体。
  • 如何将协程用于数据库任务和其他长时间运行的任务。
  • 如何使用 AdapterViewHolder 和项布局实现基本 RecyclerView
  • 如何为 RecyclerView 实现数据绑定。
  • 如何创建和使用绑定适配器来转换数据。
  • 如何使用 GridLayoutManager.

学习内容

  • 如何使 RecyclerView 中的项可点击。实现点击监听器,以便在点击某个项后导航到详情视图。

实践内容

  • 基于本系列中之前的 Codelab 内的扩展版 TrackMySleepQuality 应用进行构建。
  • 向列表中添加点击监听器并开始监听用户交互。点按某个列表项时,它会触发到包含所点击项详情的 fragment 的导航。起始代码提供了详情 fragment 的代码,以及导航代码。

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

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

此应用采用简化的架构,其中包括一个界面控制器、视图模型和 LiveData,以及一个用于保留睡眠数据的 Room 数据库。

49f975f1e5fe689.png

在此 Codelab 中,您将添加如下的响应功能:当用户点按网格中的某个项时,应用会显示类似如下所示的详情屏幕。此屏幕的代码(fragment、视图模型和导航)随起始应用一起提供,而您将实现点击处理机制。

1018f2610bca049.png

第 1 步:获取起始应用

  1. 从 GitHub 下载 RecyclerViewClickHandler-Starter 代码,并在 Android Studio 中打开该项目。
  2. 构建并运行起始睡眠跟踪应用。

[可选] 如果您想使用上一个 Codelab 中的应用,请更新您的应用

如果您要在此 Codelab 中使用 GitHub 中提供的相应起始应用,请跳至下一步。

如果您想要继续使用您在上一个 Codelab 中构建的睡眠跟踪器应用,请按照以下说明更新现有应用,使其包含详情屏幕 fragment 的代码。

  1. 即使您继续使用现有的应用,也可以从 GitHub 获取 RecyclerViewClickHandler-Starter 代码,以便复制文件。
  2. 复制 sleepdetail 软件包中的所有文件。
  3. layout 文件夹中,复制 fragment_sleep_detail.xml 文件。
  4. 复制 navigation.xml 中更新后的内容,其中增加了 sleep_detail_fragment 的导航。
  5. database 软件包中的 SleepDatabaseDao 内,添加新的 getNightWithId() 方法:
/**
 * Selects and returns the night with given nightId.
*/
@Query("SELECT * from daily_sleep_quality_table WHERE nightId = :key")
fun getNightWithId(key: Long): LiveData<SleepNight>
  1. res/values/strings 中,添加以下字符串资源:
<string name="close">Close</string>
  1. 清理并重新构建您的应用,以更新数据绑定。

第 2 步:检查睡眠详情屏幕的代码

在此 Codelab 中,您将实现睡眠之夜的点击处理程序。点击后,该应用将导航到一个 fragment,该 fragment 会显示有关特定睡眠之夜的详情。您的起始代码已包含此 SleepDetailFragment 的 fragment 和导航图,因为它包含大量代码(并且 fragment 和导航不是此 Codelab 的一部分)。请熟悉以下代码详情:

  1. 在您的应用中,找到 sleepdetail 软件包。此软件包包含 fragment、视图模型,以及 fragment 的视图模型工厂,用于显示一晚睡眠的详情。
  2. sleepdetail 软件包中,打开并检查 SleepDetailViewModel 的代码。此视图模型会在构造函数中获取 SleepNight 和 DAO 的键。

该类的主体中包含代码,用于为指定键获取 SleepNight;还包含 navigateToSleepTracker 变量,用于在按下 Close 按钮后控制导航回到 SleepTrackerFragment

getNightWithId() 函数会返回 LiveData<SleepNight>,它在 database 软件包中的 SleepDatabaseDao 内进行定义。

  1. sleepdetail 软件包中,打开并检查 SleepDetailFragment 的代码。请注意数据绑定、视图模型和导航观察器的设置。
  2. sleepdetail 软件包中,打开并检查 SleepDetailViewModelFactory 的代码。
  3. 在布局文件夹中,检查 fragment_sleep_detail.xml。请注意 <data> 标签中定义的 sleepDetailViewModel 变量,它用于从视图模型中获取要在每个视图中显示的数据。

该布局包含一个 ConstraintLayout,其中包含一个用于保存睡眠质量的 ImageView、一个用于保存质量评分的 TextView、一个用于保存睡眠时长的 TextView,以及用于关闭详情 fragment 的 Button

  1. 打开 navigation.xml 文件。对于 sleep_tracker_fragment,请注意 sleep_detail_fragment 的新操作。

新操作 action_sleep_tracker_fragment_to_sleepDetailFragment 是从睡眠跟踪器 fragment 到详情屏幕的导航。

在此任务中,您将更新 RecyclerView,通过显示所点按项的详情屏幕来响应用户点按操作。

接收和处理点击的过程由两部分组成:首先,您需要监听和接收点击,并确定被点击的是哪一项。然后,用一个操作响应点击。

那么,哪里最适合添加此应用的点击监听器呢?

  • SleepTrackerFragment 包含大量不同的 Views。在 fragment 级别监听点击事件无法获知哪一项被点击了。它甚至不会告知您被点击的是 RecyclerView 中的某个项还是其他任何界面元素。
  • RecyclerView 级别监听时,很难弄清楚用户点击了列表中的哪一项。
  • 从被点击项获取信息的最佳位置是在 ViewHolder 对象中,因为它表示一个列表项。

虽然 ViewHolder 是监听点击的绝佳位置,但它通常不是处理点击的合适位置。那么,哪里最适合处理点击呢?

  • Adapter 会在视图中显示数据项,因此您可以在适配器中处理点击。不过,从架构角度来看,适配器的作用是调整数据以供显示,而不是处理应用逻辑。
  • 您通常应该在 ViewModel. 中处理点击。ViewModel 有权访问用于确定需要对点击进行什么响应的数据和逻辑。

第 1 步:创建点击监听器并从项布局中触发它

  1. sleeptracker 软件包中,打开 SleepNightAdapter.kt
  2. 在文件末尾,创建一个新的顶级监听器类 SleepNightListener
class SleepNightListener() {

}
  1. SleepNightListener 类中,添加 onClick() 函数。当显示列表项的视图被点击时,相应视图会调用此 onClick() 函数。您稍后仍需要在布局文件中设置视图的 android:onClick 属性,该属性将在此 Codelab 的后续步骤中添加。
class SleepNightListener() {
    fun onClick() = ...
}
  1. 将类型为 SleepNight 的函数参数 night 添加到 onClick()。该视图知道它当前显示的是哪一项,并且需要传递该信息来处理点击。
class SleepNightListener() {
    fun onClick(night: SleepNight) =
}
  1. 如需指定 onClick() 的用途,请在 SleepNightListener 的构造函数中提供 onClickListener 回调参数,并将其分配给 onClick()

用于处理点击的回调应具有实用的标识符名称。不妨使用 clickListener 作为名称。clickListener 回调只需要 night.nightId 即可访问数据库中的数据。完成后的 SleepNightListener 类应如以下代码所示。

class SleepNightListener(val clickListener: (sleepId: Long) -> Unit) {
   fun onClick(night: SleepNight) = clickListener(night.nightId)
}
  1. 依次打开 res > layout > list_item_sleep_night.xml
  2. data 代码块内,添加一个新变量,使 SleepNightListener 类可通过数据绑定获得。为新的 <variable> 提供 clickListener. 这一 name。将 type 设置为 com.example.android.trackmysleepquality.sleeptracker.SleepNightListener 类的完全限定名称,如下所示。您现在可以访问这个布局中 SleepNightListener 内的 onClick() 函数。
<variable
            name="clickListener"
type="com.example.android.trackmysleepquality.sleeptracker.SleepNightListener" />
  1. 如需监听此列表项的任何部分获得的点击,请将 android:onClick 属性添加到 list_item_sleep_night.xml 布局文件中的 ConstraintLayout 内。

使用数据绑定 lambda 将此属性设置为 clickListener.onClick(sleep),如下所示:

android:onClick="@{() -> clickListener.onClick(sleep)}"

第 2 步:将点击监听器传递给 ViewHolder 和绑定对象

  1. 打开 SleepNightAdapter.kt
  2. 修改 SleepNightAdapter 类的构造函数以接收 val clickListener: SleepNightListener。当适配器绑定 ViewHolder 时,需要获取此点击监听器。
class SleepNightAdapter(val clickListener: SleepNightListener):
       ListAdapter<SleepNight, SleepNightAdapter.ViewHolder>(SleepNightDiffCallback()) {
  1. onBindViewHolder() 中,更新对 holder.bind() 的调用,将点击监听器也传递给 ViewHolder。由于您在函数调用中添加了一个参数,因此会遇到编译器错误。
holder.bind(getItem(position)!!, clickListener)
  1. 如需修复编译器错误,请将 clickListener 参数添加到 bind()。为此,请将光标放在相应错误上,然后按 Alt+Enter (Windows) 或 Option+Enter (Mac),如下面的屏幕截图所示。

b3997303d8426434.png

  1. ViewHolder 类中,将点击监听器分配给 bind() 函数内的 binding 对象。您会看到一条错误消息,因为您需要更新绑定对象。
binding.clickListener = clickListener

您从适配器构造函数获取了一个点击监听器,并将其一直传递到 ViewHolder 和绑定对象中。

  1. 如需更新数据绑定,请清理重建该项目。您可能还需要使缓存失效。Android Studio 仍可能会指示存在编译器错误,我们会在下一部分修复此错误。

第 3 步:在点按某项后显示消息框

现在,您已设置好用于捕获点击的代码,但尚未实现列表项被点按后会发生的情况。最简单的响应是在某个项被点击后显示一个内容为 nightId 的消息框。这样可以验证当列表项被点击后,是否会捕获并传递正确的 nightId

  1. 打开 SleepTrackerFragment.kt
  2. onCreateView() 中,找到 adapter 变量。请注意,这里显示存在错误,因为它现在需要一个点击监听器参数。
  3. 通过将 lambda 传入 SleepNightAdapter 来定义一个点击监听器。这个简单的 lambda 仅显示一个内容为 nightId 的消息框,如下所示。您必须导入 Toast。以下是更新后的完整定义。
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
   Toast.makeText(context, "${nightId}", Toast.LENGTH_LONG).show()
})
  1. 运行应用,点按各个项,并验证它们是否显示包含正确 nightId 的消息框。由于项的 nightId 值在不断增加,而应用首先显示的是最近一晚的数据,因此,nightId 最低的项位于列表底部。

在此任务中,您将更改当 RecyclerView 中的某项被点击后的行为,这样一来,应用不会显示消息框,而会导航到详情 fragment,其中显示了有关被点击夜晚的详细信息。

第 1 步:点击后导航

在此步骤中,您需要更改 SleepTrackerFragmentonCreateView() 中的点击监听器 lambda,以将 nightId 传递给 SleepTrackerViewModel,并触发到 SleepDetailFragment 的导航,而不只是显示消息框。

定义点击处理程序函数:

  1. 打开 SleepTrackerViewModel.kt
  2. SleepTrackerViewModel 类中,在类定义的末尾,创建 onSleepNightClicked() 点击处理程序函数。
fun onSleepNightClicked(id: Long) {

}
  1. onSleepNightClicked() 中,将 _navigateToSleepDetail 设置为被点击的睡眠之夜的传入 id,以触发导航。
fun onSleepNightClicked(id: Long) {
   _navigateToSleepDetail.value = id
}
  1. 实现 _navigateToSleepDetail。和之前一样,为导航状态定义 private MutableLiveData。还要定义一个可获取的公开 val 与之搭配。
private val _navigateToSleepDetail = MutableLiveData<Long>()
val navigateToSleepDetail
   get() = _navigateToSleepDetail
  1. 定义在应用完成导航后调用的方法。将其命名为 onSleepDetailNavigated(),并将其值设置为 null
fun onSleepDetailNavigated() {
    _navigateToSleepDetail.value = null
}

添加代码以调用点击处理程序:

  1. 打开 SleepTrackerFragment.kt,向下滚动到通过创建适配器并定义 SleepNightListener 来显示消息框的代码。
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
   Toast.makeText(context, "${nightId}", Toast.LENGTH_LONG).show()
})
  1. 在消息框下方添加以下代码,以在某个项被点按后调用 sleepTrackerViewModel 中的点击处理程序 onSleepNightClicked()。传入 nightId,以便视图模型知悉要获取哪个睡眠之夜。
sleepTrackerViewModel.onSleepNightClicked(nightId)

添加代码以观察点击情况:

  1. 打开 SleepTrackerFragment.kt
  2. onCreateView() 函数的底部,在 manager 声明的正上方,通过添加代码来观察新的 navigateToSleepDetail LiveData。当 navigateToSleepDetail 发生变化时,导航到 SleepDetailFragment,同时传入 night,然后调用 onSleepDetailNavigated()。由于您之前在上一个 Codelab 中已经执行此操作,因此代码如下:
sleepTrackerViewModel.navigateToSleepDetail.observe(viewLifecycleOwner, Observer { night ->
            night?.let {
              this.findNavController().navigate(
                        SleepTrackerFragmentDirections
                                .actionSleepTrackerFragmentToSleepDetailFragment(night))
               sleepTrackerViewModel.onSleepDetailNavigated()
            }
        })
  1. 运行代码,点击某个项,然后…应用崩溃了。

处理绑定适配器中的 null 值:

  1. 在调试模式下再次运行应用。点按某个项,然后过滤日志,以显示包含“Errors”的内容。系统会显示包含如下内容的堆栈轨迹。
Caused by: java.lang.IllegalArgumentException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull, parameter item

遗憾的是,堆栈轨迹无法清楚显示触发此错误的位置。数据绑定的一个缺点是它会使您的代码更难调试。在您点击某个项后,该应用会崩溃,而且只有新代码可以处理点击。

不过,结果是,借助这种新的点击处理机制,现在可以使用 itemnull 值调用绑定适配器。具体而言,当应用启动时,LiveData 开始时为 null,因此您需要向每个适配器添加 null 检查。

  1. BindingUtils.kt 中,针对每个绑定适配器,将 item 参数的类型更改为可为 null,并使用 item?.let{...} 封装正文。例如,sleepQualityString 的适配器将如下所示。以同样的方式更改其他适配器。
@BindingAdapter("sleepQualityString")
fun TextView.setSleepQualityString(item: SleepNight?) {
   item?.let {
       text = convertNumericQualityToString(item.sleepQuality, context.resources)
   }
}
  1. 运行应用。点按某个项,系统会打开详情视图。

Android Studio 项目:RecyclerViewClickHandler

如需使 RecyclerView 中的项响应点击,请为 ViewHolder 中的列表项附加点击监听器,并在 ViewModel 中处理点击。

如需使 RecyclerView 中的项响应点击,您需要执行以下操作:

  • 创建一个采用 lambda 并将其分配给 onClick() 函数的监听器类。
class SleepNightListener(val clickListener: (sleepId: Long) -> Unit) {
   fun onClick(night: SleepNight) = clickListener(night.nightId)
}
  • 对视图设置点击监听器。
android:onClick="@{() -> clickListener.onClick(sleep)}"
  • 将点击监听器传递给适配器构造函数,将其传入 ViewHolder,并将其添加到绑定对象中。
class SleepNightAdapter(val clickListener: SleepNightListener):
       ListAdapter<DataItem, RecyclerView.ViewHolder>(SleepNightDiffCallback()
holder.bind(getItem(position)!!, clickListener)
binding.clickListener = clickListener
  • 在显示 recycler 视图的 fragment(创建适配器的位置)中,通过将 lambda 传递给适配器来定义点击监听器。
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
      sleepTrackerViewModel.onSleepNightClicked(nightId)
})
  • 在视图模型中实现点击处理程序。对于针对列表项的点击,这通常会触发到详情 fragment 的导航。

Udacity 课程:

Android 开发者文档:

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

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

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

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

回答以下问题

问题 1

假设您的应用包含一个 RecyclerView,用于显示购物清单中的商品。您的应用还定义了一个点击监听器类:

class ShoppingListItemListener(val clickListener: (itemId: Long) -> Unit) {
    fun onClick(cartItem: CartItem) = clickListener(cartItem.itemId)
}

如何使 ShoppingListItemListener 可用于数据绑定?请选择一项。

▢ 在包含用于显示购物清单的 RecyclerView 的布局文件中,为 ShoppingListItemListener 添加一个 <data> 变量。

▢ 在为购物清单中的单行定义布局的布局文件中,为 ShoppingListItemListener 添加一个 <data> 变量。

▢ 在 ShoppingListItemListener 类中,通过添加一个函数来启用数据绑定:

fun onBinding (cartItem: CartItem) {dataBindingEnable(true)}

▢ 在 ShoppingListItemListener 类中的 onClick() 函数内,通过添加一个调用来启用数据绑定:

fun onClick(cartItem: CartItem) = {
    clickListener(cartItem.itemId)
    dataBindingEnable(true)
}

问题 2

您可以在什么位置添加 android:onClick 属性以使 RecyclerView 中的项响应点击?请选择所有适用的选项。

▢ 在显示 RecyclerView 的布局文件中,将其添加到 <androidx.recyclerview.widget.RecyclerView>

▢ 将其添加到该行中某个项的布局文件。如果您希望整个项都可点击,则将其添加到包含该行中所有项的父视图。

▢ 将其添加到该行中某个项的布局文件。如果您希望该项中的单个 TextView 可点击,请将其添加到 <TextView> 中。

▢ 始终将其添加至 MainActivity 的布局文件。

开始学习下一课:RecyclerView 中的标头