此 Codelab 是“Android Kotlin 基础知识”课程的一部分。如果您按顺序学习这些 Codelab,您将会充分发掘此课程的价值。“Android Kotlin 基础知识”Codelab 着陆页列出了所有课程 Codelab。
简介
大多数使用列表和网格显示内容的应用允许用户与内容互动。点按列表中的某个项并查看该项的详细信息,是这种交互的常见用例。为此,您可以添加点击监听器,通过显示详情视图来响应用户点按操作项。
在此 Codelab 中,您将向RecyclerView
添加互动,它基于之前一系列 Codelab 中的扩展版睡眠跟踪器应用构建。
您应当已掌握的内容
- 使用 activity、fragment 和视图构建基本界面。
- 在 fragment 之间导航,并使用
safeArgs
在 fragment 之间传递数据。 - 视图模型、视图模型工厂、转换以及
LiveData
及其观察器。 - 如何创建
Room
数据库、创建数据访问对象 (DAO) 以及定义实体。 - 如何将协程用于数据库任务和其他长时间运行的任务。
- 如何使用
Adapter
、ViewHolder
和项布局实现基本RecyclerView
。 - 如何为
RecyclerView
实现数据绑定。 - 如何创建和使用绑定适配器来转换数据。
- 如何使用
GridLayoutManager
.
学习内容
- 如何使
RecyclerView
中的项可点击。实现点击监听器,以便在点击某个项后导航到详情视图。
您将执行的操作
- 基于本系列上一个 Codelab 中的 TrackMySleepQuality 应用扩展版本构建。
- 向列表中添加点击监听器并开始监听用户交互。点按某个列表项时,它会触发到包含所点击项详情的 fragment 的导航。起始代码提供了详情 fragment 的代码,以及导航代码。
起始睡眠跟踪器应用有两个屏幕(由 fragment 表示),如下图所示。
左侧所示的第一个屏幕包含用于开始和停止跟踪的按钮。这个屏幕会显示用户的一些睡眠数据。CLEAR 按钮用于永久删除应用针对用户收集的所有数据。右侧所示的第二个屏幕用于选择睡眠质量评分。
此应用使用简化的架构,其中包含界面控制器、视图模型和 LiveData
,以及 Room
数据库以持久存储睡眠数据。
在此 Codelab 中,您将添加如下的响应功能:当用户点按网格中的某个项时,应用会显示类似如下所示的详情屏幕。此屏幕的代码(fragment、视图模型和导航)随起始应用一起提供,而您将实现点击处理机制。
第 1 步:获取起始应用
- 从 GitHub 下载 RecyclerViewClickHandler-Starter 代码并在 Android Studio 中打开项目。
- 构建并运行起始睡眠跟踪器应用。
[可选] 如果您想使用上一个 Codelab 中的应用,请更新您的应用
如果您要在此 Codelab 中使用 GitHub 中提供的相应起始应用,请跳至下一步。
如果您想要继续使用您在上一个 Codelab 中构建的睡眠跟踪器应用,请按照以下说明更新现有应用,使其包含详情屏幕 fragment 的代码。
- 即使您继续使用现有应用,也请从 GitHub 获取 RecyclerViewClickHandler-Starter 代码,以便您可以复制文件。
- 复制
sleepdetail
软件包中的所有文件。 - 在
layout
文件夹中,复制文件fragment_sleep_detail.xml
。 - 复制
navigation.xml
的更新内容,为sleep_detail_fragment
添加导航。 - 在
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>
- 在
res/values/strings
中添加以下字符串资源:
<string name="close">Close</string>
- 清理并重建应用以更新数据绑定。
第 2 步:检查睡眠详情屏幕的代码
在此 Codelab 中,您将实现一个点击处理程序,该 fragment 会导航到显示有关所点击睡眠之夜的 fragment 的信息。您的起始代码已包含此 SleepDetailFragment
的 fragment 和导航图,因为它包含大量代码,并且 fragment 和 Navigation 不属于此 Codelab。熟悉以下代码:
- 在您的应用中,找到
sleepdetail
软件包。此软件包包含某个 fragment 的 fragment、视图模型和视图模型工厂,该 fragment 会显示一晚的睡眠详情。 - 在
sleepdetail
软件包中,打开并检查SleepDetailViewModel
的代码。此视图模型在构造函数中接受SleepNight
和 DAO 的键。
类的正文具有用于获取给定键的SleepNight
的代码,以及用于控制在按下关闭按钮时导航回SleepTrackerFragment
的navigateToSleepTracker
变量。getNightWithId()
函数会返回LiveData<SleepNight>
,并在SleepDatabaseDao
(在database
软件包中)中定义。 - 在
sleepdetail
软件包中,打开并检查SleepDetailFragment
的代码。请注意用于数据绑定、视图模型和导航观察器的设置。 - 在
sleepdetail
软件包中,打开并检查SleepDetailViewModelFactory
的代码。 - 在布局文件夹中,检查
fragment_sleep_detail.xml
。请注意,在<data>
标记中定义的sleepDetailViewModel
变量用于获取视图模型中每个视图中显示的数据。
布局包含ConstraintLayout
,它包含睡眠质量的ImageView
、质量评分的TextView
、睡眠时长的TextView
,以及用于关闭详情 fragment 的Button
。 - 打开
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 步:创建点击监听器并从项布局中触发它
- 在
sleeptracker
文件夹中,打开 SleepNightAdapter.kt。 - 在文件末尾,创建新的监听器类
SleepNightListener
。
class SleepNightListener() {
}
- 在
SleepNightListener
类中,添加onClick()
函数。当显示列表项的视图被点击时,相应视图会调用此onClick()
函数。(您稍后需要将视图的android:onClick
属性设置为此函数。)
class SleepNightListener() {
fun onClick() =
}
- 将类型为
SleepNight
的函数参数night
添加到onClick()
。视图知道当前显示的项目,且需要传递该信息来处理点击。
class SleepNightListener() {
fun onClick(night: SleepNight) =
}
- 如需定义
onClick()
的用途,请在SleepNightListener
的构造函数中提供clickListener
回调并将其分配给onClick()
。
为处理点击的 lambda 命名clickListener
有助于在类之间传递名称时对其进行跟踪。clickListener
回调只需使用night.nightId
即可从数据库访问数据。完成后的SleepNightListener
类应如以下代码所示。
class SleepNightListener(val clickListener: (sleepId: Long) -> Unit) {
fun onClick(night: SleepNight) = clickListener(night.nightId)
}
- 打开 list_item_sleep_night.xml。
- 在
data
代码块内,添加一个新变量,使SleepNightListener
类可通过数据绑定获得。为新的<variable>
提供clickListener.
这一name
。将type
设置为com.example.android.trackmysleepquality.sleeptracker.SleepNightListener
类的完全限定名称,如下所示。您现在可以访问这个布局中SleepNightListener
内的onClick()
函数。
<variable
name="clickListener"
type="com.example.android.trackmysleepquality.sleeptracker.SleepNightListener" />
- 如需监听此列表项的任何部分获得的点击次数,请将
android:onClick
属性添加到ConstraintLayout
。
使用数据绑定 lambda 将属性设置为clickListener:onClick(sleep)
,如下所示:
android:onClick="@{() -> clickListener.onClick(sleep)}"
第 2 步:将点击监听器传递给 ViewHolder 和绑定对象
- 打开 SleepNightAdapter.kt。
- 修改
SleepNightAdapter
类的构造函数以接收val clickListener: SleepNightListener
。当适配器绑定ViewHolder
时,需要获取此点击监听器。
class SleepNightAdapter(val clickListener: SleepNightListener):
ListAdapter<SleepNight, SleepNightAdapter.ViewHolder>(SleepNightDiffCallback()) {
- 在
onBindViewHolder()
中,更新对holder.bind()
的调用,将点击监听器也传递给ViewHolder
。由于您在函数调用中添加了一个参数,因此会遇到编译器错误。
holder.bind(getItem(position)!!, clickListener)
- 将
clickListener
参数添加到bind()
。为此,请将光标置于该错误上,然后在错误上按Alt+Enter
(Windows) 或Option+Enter
(Mac),以便转到 ,如以下屏幕截图所示。
- 在
ViewHolder
类中的bind()
函数内,将点击监听器分配给binding
对象。您会看到一条错误消息,因为您需要更新绑定对象。
binding.clickListener = clickListener
- 如需更新数据绑定,请清理并重新构建您的项目。(您可能还需要使缓存失效)。因此,您已从适配器构造函数获取了点击监听器,并将其一直传递给 ViewHolder 并传入绑定对象。
第 3 步:在用户点按商品时显示消息框
现在,您已设置好用于捕获点击的代码,但尚未实现列表项被点按后会发生的情况。最简单的响应是在某个项被点击后显示一个内容为 nightId
的消息框。这样可以验证当列表项被点击后,是否会捕获并传递正确的 nightId
。
- 打开 SleepTrackerFragment.kt。
- 在
onCreateView()
中,找到adapter
变量。请注意,它显示一个错误,因为它现在需要点击监听器参数。 - 通过将 lambda 传入
SleepNightAdapter
来定义一个点击监听器。这个简单的 lambda 仅显示一个内容为nightId
的消息框,如下所示。您必须导入Toast
。以下是更新后的完整定义。
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
Toast.makeText(context, "${nightId}", Toast.LENGTH_LONG).show()
})
- 运行应用,点按各个项,并验证它们是否显示包含正确
nightId
的消息框。由于这些项的nightId
值在增加,并且应用最先显示最近一晚,因此nightId
最低的项会显示在列表底部。
在此任务中,您将更改点击 RecyclerView
中的项时的行为,以便应用导航到显示有关所点击之夜的更多信息的详细信息 fragment,而不是显示消息框。
第 1 步:点击后导航
在此步骤中,您需要更改 SleepTrackerFragment
的 onCreateView()
中的点击监听器 lambda,以将 nightId
传递给 SleepTrackerViewModel
,并触发到 SleepDetailFragment
的导航,而不只是显示消息框。
定义点击处理程序函数:
- 打开 SleepTrackerViewModel.kt。
- 在
SleepTrackerViewModel
内部,定义onSleepNightClicked()
点击处理程序函数。
fun onSleepNightClicked(id: Long) {
}
- 在
onSleepNightClicked()
中,将_navigateToSleepDetail
设置为被点击的睡眠之夜的传入id
,以触发导航。
fun onSleepNightClicked(id: Long) {
_navigateToSleepDetail.value = id
}
- 实现
_navigateToSleepDetail
。和之前一样,为导航状态定义private MutableLiveData
。它还有一个公开的 getableval
。
private val _navigateToSleepDetail = MutableLiveData<Long>()
val navigateToSleepDetail
get() = _navigateToSleepDetail
- 定义在应用完成导航后调用的方法。将其命名为
onSleepDetailNavigated()
,并将其值设置为null
。
fun onSleepDetailNavigated() {
_navigateToSleepDetail.value = null
}
添加代码以调用点击处理程序:
- 打开 SleepTrackerFragment.kt,向下滚动到通过创建适配器并定义
SleepNightListener
来显示消息框的代码。
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
Toast.makeText(context, "${nightId}", Toast.LENGTH_LONG).show()
})
- 在消息框下方添加以下代码,以在某个项被点按后调用
sleepTrackerViewModel
中的点击处理程序onSleepNighClicked()
。传入nightId
,以便视图模型知道要获得哪个睡眠晚上。这会使您产生错误,因为您尚未定义onSleepNightClicked()
。您可以根据需要保留、评论或删除消息框。
sleepTrackerViewModel.onSleepNightClicked(nightId)
添加代码以观察点击情况:
- 打开 SleepTrackerFragment.kt。
- 在
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()
}
})
- 运行代码,点击项目,然后应用崩溃。
处理绑定适配器中的 null 值:
- 在调试模式下再次运行应用。点按某个项,然后过滤日志,以显示包含“Errors”的内容。系统将显示包含以下内容的堆栈轨迹。
Caused by: java.lang.IllegalArgumentException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull, parameter item
遗憾的是,堆栈轨迹无法清楚显示触发此错误的位置。数据绑定的一个缺点是它会使您的代码更难调试。当您点击某个项时,应用会崩溃,并且唯一新代码就是用于处理点击。
然而,现在有了这种新的点击处理机制,就可以使用 item
的 null
值调用绑定适配器。特别是,在应用启动时,LiveData
以 null
开头,因此您需要向每个适配器添加 null 检查。
- 在
BindingUtils.kt
中,针对每个绑定适配器,将item
参数的类型更改为可为 null,并使用item?.let{...}
封装正文。例如,sleepQualityString
的适配器将如下所示。以同样的方式更改其他适配器。
@BindingAdapter("sleepQualityString")
fun TextView.setSleepQualityString(item: SleepNight?) {
item?.let {
text = convertNumericQualityToString(item.sleepQuality, context.resources)
}
}
- 运行您的应用。点按相应项目,系统会打开一个详情视图。
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
添加布局文件。
开始学习下一课: