Android 入门教程
RecyclerView 进阶
本教程共 100 篇 · 第 61 篇 · 更新于 2026-07-28 · 约 8 分钟阅读
AndroidAndroid 入门教程RecyclerViewListAdapterDiffUtilItemTouchHelper多ViewType
61. RecyclerView 进阶
本节目标:学会用 ListAdapter + DiffUtil 高效更新列表、实现多 ViewType 混合列表、用 ItemTouchHelper 做拖拽和滑动删除。
ListAdapter + DiffUtil
上一章的 Adapter 用 notifyDataSetChanged() 或手动 notifyItemXxx() 更新数据,要么性能差,要么容易算错 position。
ListAdapter 内置 DiffUtil,自动计算新旧列表差异,只更新真正变化的项,还自带增删动画。
实现
class ContactAdapter(
private val onItemClick: (Contact) -> Unit
) : ListAdapter<Contact, ContactViewHolder>(DIFF_CALLBACK) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ContactViewHolder {
val binding = ItemContactBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return ContactViewHolder(binding, onItemClick)
}
override fun onBindViewHolder(holder: ContactViewHolder, position: Int) {
holder.bind(getItem(position)) // 用 getItem 而非 list[position]
}
companion object {
val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Contact>() {
override fun areItemsTheSame(oldItem: Contact, newItem: Contact): Boolean {
// 判断是不是同一个对象(用 id)
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Contact, newItem: Contact): Boolean {
// 判断内容是否完全相同
return oldItem == newItem
}
}
}
}
使用
val adapter = ContactAdapter { contact ->
// 点击处理
}
recyclerView.adapter = adapter
// 更新数据:直接 submitList,DiffUtil 自动算差异
adapter.submitList(newContacts)
Note
areItemsTheSame判断两个对象是否代表同一条数据(用 id)。areContentsTheSame在前者为 true 时再判断内容是否完全相同。只有 id 相同但内容不同的项才会被局部更新。这是 DiffUtil 的核心:先用 id 快速筛出「哪些项是新的/删除的/移动的」,再比较内容决定「哪些项需要更新」。
payload 局部更新
如果只想更新某一项的部分内容(比如只改名字不改头像),用 payload:
override fun areContentsTheSame(oldItem: Contact, newItem: Contact): Boolean {
return oldItem == newItem
}
override fun getChangePayload(oldItem: Contact, newItem: Contact): Any? {
// 只返回变化的部分
if (oldItem.name != newItem.name) return "name"
return null
}
override fun onBindViewHolder(
holder: ContactViewHolder,
position: Int,
payloads: MutableList<Any>
) {
if (payloads.contains("name")) {
// 只更新名字,不重新绑定整个 ViewHolder
holder.updateName(getItem(position).name)
} else {
onBindViewHolder(holder, position)
}
}
多 ViewType
一个列表里混合多种布局,比如聊天列表里有文字消息和图片消息。
数据模型
sealed class Message {
abstract val id: String
data class TextMessage(override val id: String, val text: String) : Message()
data class ImageMessage(override val id: String, val imageUrl: String) : Message()
}
Adapter
class MessageAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val items = mutableListOf<Message>()
companion object {
const val TYPE_TEXT = 1
const val TYPE_IMAGE = 2
}
override fun getItemViewType(position: Int): Int {
return when (items[position]) {
is Message.TextMessage -> TYPE_TEXT
is Message.ImageMessage -> TYPE_IMAGE
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
TYPE_TEXT -> {
val binding = ItemTextMessageBinding.inflate(inflater, parent, false)
TextMessageViewHolder(binding)
}
TYPE_IMAGE -> {
val binding = ItemImageMessageBinding.inflate(inflater, parent, false)
ImageMessageViewHolder(binding)
}
else -> throw IllegalArgumentException("未知类型")
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (val item = items[position]) {
is Message.TextMessage -> (holder as TextMessageViewHolder).bind(item)
is Message.ImageMessage -> (holder as ImageMessageViewHolder).bind(item)
}
}
override fun getItemCount() = items.size
fun submit(newItems: List<Message>) {
items.clear()
items.addAll(newItems)
notifyDataSetChanged()
}
}
Tip
- 用
sealed class做数据模型比用interface好:when表达式能编译期检查完整性,加了新类型忘了处理会报错。多 ViewType 的关键是getItemViewType()返回类型标识,onCreateViewHolder根据类型创建不同 ViewHolder。
ItemTouchHelper 拖拽和滑动
实现拖拽排序和左滑删除:
val itemTouchHelper = ItemTouchHelper(object : ItemTouchHelper.Callback() {
// 允许拖拽和滑动的方向
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
val swipeFlags = ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
return makeMovementFlags(dragFlags, swipeFlags)
}
// 拖拽回调
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val from = viewHolder.bindingAdapterPosition
val to = target.bindingAdapterPosition
Collections.swap(items, from, to)
adapter.notifyItemMoved(from, to)
return true
}
// 滑动回调
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.bindingAdapterPosition
items.removeAt(position)
adapter.notifyItemRemoved(position)
}
})
itemTouchHelper.attachToRecyclerView(recyclerView)
长按拖拽
默认需要长按才能拖拽。设置 isLongPressDragEnabled:
override fun isLongPressDragEnabled() = true
override fun isItemViewSwipeEnabled() = true
Note
bindingAdapterPosition是最新的 position(替代已废弃的adapterPosition)。在onMove和onSwiped里用它获取当前位置。
Header 和 Footer
RecyclerView 没有内置 Header/Footer 支持,通过多 ViewType 实现:
companion object {
const val TYPE_HEADER = 0
const val TYPE_ITEM = 1
}
override fun getItemViewType(position: Int): Int {
return if (position == 0) TYPE_HEADER else TYPE_ITEM
}
override fun getItemCount(): Int {
return items.size + 1 // 多一个 header
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is ItemViewHolder) {
// position - 1 因为第一个是 header
holder.bind(items[position - 1])
}
}
Warning
- 手动管理 position 偏移容易出错(加 header 后所有 position 都要 -1)。如果用
ListAdapter,getItem()会自动处理,更安全。
小结
ListAdapter + DiffUtil 是列表更新的最佳实践,自动算差异、自带动画。多 ViewType 用 sealed class 建模,getItemViewType 路由。ItemTouchHelper 做拖拽排序和滑动删除。Header/Footer 通过多 ViewType 实现。这些是 RecyclerView 的进阶技能,维护老项目时常用。下一节学习 ViewPager 和导航。