11、点击事件处理(一):在 Adapter 中设置 OnClickListener
点击事件,是列表交互的灵魂。没有点击的列表,就像没有按钮的遥控器——摆着好看,但没法用。
今天我们来聊聊,在 RecyclerView 的 Adapter 里,怎么给每个 item 绑定点击事件。我最早接触 RecyclerView 时,第一反应是「直接在 item 的 XML 里写 android:onClick 不就行了?」嗯,后来发现这条路走不通。RecyclerView 的 item 是动态回收复用的,你没法像 Activity 里那样直接绑一个方法名。
那怎么办?最经典的做法,就是在 Adapter 内部设置 OnClickListener。说白了,就是在 onBindViewHolder 的时候,给 itemView 或者 itemView 里的某个子 View 注册点击监听。
11.1 基础写法:在 onBindViewHolder 中设置监听
先看一个最直接的写法。假设我们有一个简单的列表,展示用户姓名,点击后弹出一个 Toast。
class UserAdapter(private val userList: List<User>) :
RecyclerView.Adapter<UserAdapter.UserViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_user, parent, false)
return UserViewHolder(view)
}
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
val user = userList[position]
holder.bind(user)
// 直接在 onBindViewHolder 中设置点击事件
holder.itemView.setOnClickListener {
Toast.makeText(
holder.itemView.context,
"点击了:${user.name}",
Toast.LENGTH_SHORT
).show()
}
}
override fun getItemCount() = userList.size
class UserViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val tvName: TextView = itemView.findViewById(R.id.tvName)
fun bind(user: User) {
tvName.text = user.name
}
}
}
这段代码能跑,但有个小问题。每次 onBindViewHolder 被调用时,都会重新设置一次 OnClickListener。虽然 RecyclerView 会复用 ViewHolder,但 onBindViewHolder 在 item 滚动进出屏幕时会频繁触发。你想想看,一个 ViewHolder 可能被绑定 10 次,那就注册了 10 次监听器。虽然不会导致崩溃,但性能上不优雅。
11.2 优化方案:在 ViewHolder 中注册监听
把点击事件的注册挪到 ViewHolder 的初始化阶段。这样每个 ViewHolder 只注册一次监听,不管它被绑定多少次。
class UserAdapter(private val userList: List<User>) :
RecyclerView.Adapter<UserAdapter.UserViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_user, parent, false)
return UserViewHolder(view)
}
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
holder.bind(userList[position])
}
override fun getItemCount() = userList.size
class UserViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val tvName: TextView = itemView.findViewById(R.id.tvName)
init {
// 一次性注册点击监听
itemView.setOnClickListener {
val position = adapterPosition
if (position != RecyclerView.NO_POSITION) {
Toast.makeText(
itemView.context,
"点击了第 ${position + 1} 项",
Toast.LENGTH_SHORT
).show()
}
}
}
fun bind(user: User) {
tvName.text = user.name
}
}
}
这里有个细节要注意:adapterPosition 在 init 块里是拿不到的,因为 ViewHolder 还没绑定数据。但点击事件是延迟触发的,用户点击时 ViewHolder 已经绑定了数据,所以 adapterPosition 能正确返回当前的位置。
adapterPosition 来获取数据。因为 init 执行时,ViewHolder 还没有和任何数据关联。我曾经见过有人这么写,结果点击时永远返回的是 -1 或者上一次的位置。嗯,踩过这个坑。
11.3 传递点击事件到 Activity 或 Fragment
在实际项目中,点击事件往往需要通知到外部——比如跳转页面、弹出对话框、或者更新 ViewModel 里的数据。这时候就需要把点击事件通过回调接口传递出去。
定义一个接口:
interface OnItemClickListener {
fun onItemClick(user: User, position: Int)
}
在 Adapter 中持有这个接口的引用:
class UserAdapter(
private val userList: List<User>,
private val listener: OnItemClickListener
) : RecyclerView.Adapter<UserAdapter.UserViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_user, parent, false)
return UserViewHolder(view)
}
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
holder.bind(userList[position])
}
override fun getItemCount() = userList.size
inner class UserViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val tvName: TextView = itemView.findViewById(R.id.tvName)
init {
itemView.setOnClickListener {
val position = adapterPosition
if (position != RecyclerView.NO_POSITION) {
listener.onItemClick(userList[position], position)
}
}
}
fun bind(user: User) {
tvName.text = user.name
}
}
}
在 Activity 或 Fragment 中使用:
val adapter = UserAdapter(userList, object : OnItemClickListener {
override fun onItemClick(user: User, position: Int) {
Toast.makeText(this@MainActivity, "点击了 ${user.name}", Toast.LENGTH_SHORT).show()
// 可以在这里做跳转、更新数据等操作
}
})
recyclerView.adapter = adapter
11.4 点击事件与数据绑定的关系
如果你用了 DataBinding 或 ViewBinding,点击事件的处理方式会略有不同。但底层逻辑是一样的——最终还是通过 setOnClickListener 来注册。
我个人建议,在项目初期或者简单场景下,直接用回调接口的方式。等列表逻辑复杂了,再考虑引入 DataBinding 或者 DiffUtil 等高级特性。不要一开始就上重型武器,容易把自己绕晕。
11.5 本章知识体系
下面这张图展示了点击事件处理的核心流程和关键决策点:
从图中可以看出,核心决策在于「在哪里注册监听」。我个人强烈推荐在 ViewHolder 的 init 块中注册,然后在回调中通过 adapterPosition 获取当前位置。这样既保证了性能,又保持了代码的清晰度。
好了,点击事件的基础处理就聊到这里。记住一个原则:监听器只注册一次,数据通过回调传递。这样你的 Adapter 既高效又易于维护。