Android Kotlin 基础知识 07.4:与 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 数据库以持久存储睡眠数据。

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

第 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 和 Navigation 不属于此 Codelab。熟悉以下代码:

  1. 在您的应用中,找到 sleepdetail 软件包。此软件包包含某个 fragment 的 fragment、视图模型和视图模型工厂,该 fragment 会显示一晚的睡眠详情。

  2. sleepdetail 软件包中,打开并检查 SleepDetailViewModel 的代码。此视图模型在构造函数中接受 SleepNight 和 DAO 的键。

    类的正文具有用于获取给定键的 SleepNight 的代码,以及用于控制在按下关闭按钮时导航回 SleepTrackerFragmentnavigateToSleepTracker 变量。

    getNightWithId() 函数会返回 LiveData<SleepNight>,并在 SleepDatabaseDao(在 database 软件包中)中定义。

  3. sleepdetail 软件包中,打开并检查 SleepDetailFragment 的代码。请注意用于数据绑定、视图模型和导航观察器的设置。

  4. sleepdetail 软件包中,打开并检查 SleepDetailViewModelFactory 的代码。

  5. 在布局文件夹中,检查 fragment_sleep_detail.xml。请注意,在 <data> 标记中定义的 sleepDetailViewModel 变量用于获取视图模型中每个视图中显示的数据。

    布局包含 ConstraintLayout,它包含睡眠质量的 ImageView、质量评分的 TextView、睡眠时长的 TextView,以及用于关闭详情 fragment 的 Button

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

    新操作 action_sleep_tracker_fragment_to_sleepDetailFragment 是从睡眠跟踪器 fragment 导航到详细信息屏幕。

在此任务中,您将通过更新被点按项的详细信息屏幕来更新 RecyclerView 以响应用户点按操作。

接收点击次数并进行处理包含两个部分任务:首先,您需要监听并接收点击,确定用户点击的是哪个项目。然后,您需要对操作进行响应。

那么,要为此应用添加点击监听器的最佳位置是什么?

  • SleepTrackerFragment 会托管许多视图,因此在 fragment 级别监听点击事件并不会告诉您用户点击的是哪个项目。它甚至不会告诉您是用户点击了内容还是其他某个界面元素。
  • RecyclerView 级别监听时,很难弄清楚用户点击了列表中的哪一项。
  • 获取关于所点击项的信息的最佳速度是在 ViewHolder 对象中,因为它表示一个列表项。

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

  • Adapter 会在视图中显示数据项,因此您可以在适配器中处理点击。不过,适配器的作用是调整数据以供显示,而不是处理应用逻辑。
  • 您通常应该在 ViewModel 中处理点击,因为 ViewModel 可以访问相关数据和逻辑,以便确定应对点击所需执行的操作。

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

  1. sleeptracker 文件夹中,打开 SleepNightAdapter.kt
  2. 在文件末尾,创建新的监听器类 SleepNightListener
class SleepNightListener() {
    
}
  1. SleepNightListener 类中,添加 onClick() 函数。当显示列表项的视图被点击时,相应视图会调用此 onClick() 函数。(您稍后需要将视图的 android:onClick 属性设置为此函数。)
class SleepNightListener() {
    fun onClick() = 
}
  1. 将类型为 SleepNight 的函数参数 night 添加到 onClick()。视图知道当前显示的项目,且需要传递该信息来处理点击。
class SleepNightListener() {
    fun onClick(night: SleepNight) = 
}
  1. 如需定义 onClick() 的用途,请在 SleepNightListener 的构造函数中提供 clickListener 回调并将其分配给 onClick()

    为处理点击的 lambda 命名clickListener有助于在类之间传递名称时对其进行跟踪。clickListener 回调只需使用 night.nightId 即可从数据库访问数据。完成后的 SleepNightListener 类应如以下代码所示。
class SleepNightListener(val clickListener: (sleepId: Long) -> Unit) {
   fun onClick(night: SleepNight) = clickListener(night.nightId)
}
  1. 打开 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 属性添加到 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),以便转到 ,如以下屏幕截图所示。

  1. ViewHolder 类中的 bind() 函数内,将点击监听器分配给 binding 对象。您会看到一条错误消息,因为您需要更新绑定对象。
binding.clickListener = clickListener
  1. 如需更新数据绑定,请清理重新构建您的项目。(您可能还需要使缓存失效)。因此,您已从适配器构造函数获取了点击监听器,并将其一直传递给 ViewHolder 并传入绑定对象。

第 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。它还有一个公开的 getable 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 中的点击处理程序 onSleepNighClicked()。传入 nightId,以便视图模型知道要获得哪个睡眠晚上。这会使您产生错误,因为您尚未定义 onSleepNightClicked()。您可以根据需要保留、评论或删除消息框。
sleepTrackerViewModel.onSleepNightClicked(nightId)

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

  1. 打开 SleepTrackerFragment.kt
  2. onCreateView() 中的 manager 声明的正上方,添加代码以观察新的 navigateToSleepDetail LiveData。当 navigateToSleepDetail 发生变化时,导航到 SleepDetailFragment,同时传入 night,然后调用 onSleepDetailNavigated()。由于您之前在上一个 Codelab 中已执行此操作,因此代码如下:
sleepTrackerViewModel.navigateToSleepDetail.observe(this, 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 值调用绑定适配器。特别是,在应用启动时,LiveDatanull 开头,因此您需要向每个适配器添加 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 添加布局文件。

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