首页 / Android 入门教程 / RecyclerView 进阶

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)。在 onMoveonSwiped 里用它获取当前位置。

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)。如果用 ListAdaptergetItem() 会自动处理,更安全。

小结

ListAdapter + DiffUtil 是列表更新的最佳实践,自动算差异、自带动画。多 ViewType 用 sealed class 建模,getItemViewType 路由。ItemTouchHelper 做拖拽排序和滑动删除。Header/Footer 通过多 ViewType 实现。这些是 RecyclerView 的进阶技能,维护老项目时常用。下一节学习 ViewPager 和导航。