第 7 篇|列表王者 RecyclerView 完全指南 —— 从新手到灵活运用

3 阅读8分钟

列表王者 RecyclerView 完全指南 —— 从新手到灵活运用

彻底掌握 RecyclerView,替代 ListView,实现高性能列表开发


哈喽,各位坚持学习的小伙伴们!上一篇我们攻克了 Fragment,现在你的 App 已经可以搭出灵活的页面架构了。但还有一个几乎所有 App 都绕不开的核心组件 —— 列表。

无论是朋友圈的动态流、淘宝的商品列表、还是备忘录的条目,背后都是同一个东西:RecyclerView。它是 Google 官方推出的高性能列表控件,用来取代老旧的 ListView,也是面试必问、开发必会的基础技能。

今天,我们就从零开始,把 RecyclerView 的原理、搭建步骤、点击事件、性能优化、常见踩坑一次性讲透,最后再做一个带删除动画的备忘录实战案例。

全程附 Kotlin 代码,保证新手也能跑起来!


一、RecyclerView 是什么?为什么能淘汰 ListView?

1.1 从 ListView 到 RecyclerView 的进化

ListView 作为 Android 早期的列表组件,存在诸多无法解决的硬伤:

对比维度ListViewRecyclerView
布局能力仅支持垂直线性布局支持线性、网格、瀑布流、横向滑动,可自定义
缓存机制二级缓存,仅缓存视图四级缓存,缓存视图 + 数据,复用率极高
解耦设计高度耦合,布局、动画、分割线混在一起完全解耦,LayoutManager、ItemDecoration、ItemAnimator 各司其职
刷新机制只能全局刷新,性能差支持局部刷新,自带增删改移动画
扩展性差,自定义复杂极强,可插拔式扩展任意功能

1.2 RecyclerView 四级缓存机制(性能核心)

RecyclerView 高性能的根本原因,就是它的四级缓存复用体系:

  • 一级缓存(Scrap):缓存屏幕内正在显示的 item,直接复用,无需重新绑定数据。
  • 二级缓存(Cache):缓存刚滑出屏幕的 item,优先复用,也无需重新绑定。
  • 三级缓存(ViewCacheExtension):自定义缓存,开发者可按需扩展。
  • 四级缓存(RecycledViewPool):跨类型共享的视图池,多个 RecyclerView 可共用一个 Pool。

通俗理解:ListView 滑出屏幕的 item 会被销毁重建,频繁触发 findViewById。RecyclerView 把它们放进缓存池,新滑入的 item 直接从池里取出来换个数据就能用,视图不用重建,性能自然完胜。


二、RecyclerView 的核心成员

在写代码之前,先认识四位核心成员:

成员职责
Item 布局单个列表条目的 XML 布局文件
ViewHolder持有条目中所有控件的引用,避免反复 findViewById
Adapter数据与视图的桥梁:创建 ViewHolder、绑定数据、返回条目数量
LayoutManager决定条目的排列方式(线性、网格、瀑布流)

四者缺一不可,必须全部就位,RecyclerView 才能正常工作。


三、四步搭建第一个 RecyclerView 列表

我们以「备忘录列表」为例,逐步完成搭建。

步骤 1:添加依赖

在 app/build.gradle.kts 中添加 RecyclerView 依赖(一般新项目已默认包含):

dependencies {
    implementation("androidx.recyclerview:recyclerview:1.3.2")
}

步骤 2:编写 Item 布局

每个备忘录条目显示标题和时间两行文字,创建 item_memo.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="16dp"
    android:background="?attr/selectableItemBackground"
    android:clickable="true">

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="16sp"
        android:textColor="@android:color/black"
        android:maxLines="1"
        android:ellipsize="end" />

    <TextView
        android:id="@+id/tv_time"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="12sp"
        android:textColor="@android:color/darker_gray"
        android:layout_marginTop="4dp" />

</LinearLayout>

步骤 3:编写 ViewHolder 和 Adapter

// 数据类
data class Memo(val id: Int, val title: String, val time: String)

// ViewHolder:持有每个条目中的控件引用,只需初始化一次
class MemoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    val tvTitle: TextView = itemView.findViewById(R.id.tv_title)
    val tvTime: TextView = itemView.findViewById(R.id.tv_time)
}

// Adapter:创建 ViewHolder 并绑定数据
class MemoAdapter(
    private val memoList: MutableList<Memo>,
    private val onItemClick: (Int) -> Unit,
    private val onItemLongClick: (Int) -> Boolean
) : RecyclerView.Adapter<MemoViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MemoViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.item_memo, parent, false)
        // 在 ViewHolder 构造时绑定点击事件,避免在 onBindViewHolder 中重复设置
        return MemoViewHolder(view).apply {
            itemView.setOnClickListener {
                val pos = adapterPosition
                if (pos != RecyclerView.NO_POSITION) onItemClick(pos)
            }
            itemView.setOnLongClickListener {
                val pos = adapterPosition
                if (pos != RecyclerView.NO_POSITION) onItemLongClick(pos) else false
            }
        }
    }

    override fun onBindViewHolder(holder: MemoViewHolder, position: Int) {
        val memo = memoList[position]
        holder.tvTitle.text = memo.title
        holder.tvTime.text = memo.time
    }

    override fun getItemCount() = memoList.size

    fun removeItem(position: Int) {
        memoList.removeAt(position)
        notifyItemRemoved(position)
    }
}

步骤 4:设置 LayoutManager 并绑定 RecyclerView

在 Activity 布局中放置 RecyclerView:

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/rv_memo"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

在 Activity 中初始化:

class MainActivity : AppCompatActivity() {

    private val memoList = mutableListOf<Memo>()
    private lateinit var adapter: MemoAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        loadTestData()

        adapter = MemoAdapter(
            memoList,
            onItemClick = { pos ->
                Toast.makeText(this, memoList[pos].title, Toast.LENGTH_SHORT).show()
            },
            onItemLongClick = { pos ->
                adapter.removeItem(pos)
                Toast.makeText(this, "已删除", Toast.LENGTH_SHORT).show()
                true
            }
        )

        findViewById<RecyclerView>(R.id.rv_memo).apply {
            layoutManager = LinearLayoutManager(this@MainActivity)
            adapter = this@MainActivity.adapter
            itemAnimator = DefaultItemAnimator()   // 开启默认删除/插入动画
            setHasFixedSize(true)                  // 固定高度优化,提升性能
        }
    }

    private fun loadTestData() {
        for (i in 1..20) {
            memoList.add(Memo(i, "备忘录标题 $i", "2025-05-20 10:$i"))
        }
    }
}

至此,一个带有点击、长按删除和默认动画的备忘录列表就跑起来了。


四、点击事件的标准实现

RecyclerView 没有像 Button 那样默认的点击监听,需要我们自己在 Adapter 中实现。

关键原则:点击事件应在 ViewHolder 构造函数中绑定,不能在 onBindViewHolder 中重复设置,否则每次绑定数据都会创建新的监听器对象,造成性能浪费。

// ✅ 正确:在 ViewHolder 构造时绑定一次
class MemoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    init {
        itemView.setOnClickListener { ... }
        itemView.setOnLongClickListener { ... }
    }
}

// ❌ 错误:在 onBindViewHolder 中每次绑定都重新设置
override fun onBindViewHolder(holder: MemoViewHolder, position: Int) {
    holder.itemView.setOnClickListener { ... } // 重复创建监听器
}

回调时用 adapterPosition 获取位置,并过滤 NO_POSITION(-1)这个无效值。


五、性能优化:DiffUtil 替代 notifyDataSetChanged

5.1 notifyDataSetChanged 的致命缺点

  • 无差别刷新所有可见 item,即使数据没变
  • 没有任何动画效果
  • 频繁刷新会导致滑动卡顿

5.2 DiffUtil:精准刷新 + 自带动画

DiffUtil 会对比新旧数据集的差异,只刷新发生变化的条目,并自动附带增删移动画。

创建 DiffUtil.Callback:

class MemoDiffCallback : DiffUtil.ItemCallback<Memo>() {
    override fun areItemsTheSame(oldItem: Memo, newItem: Memo): Boolean {
        return oldItem.id == newItem.id // 用唯一 ID 判断是否是同一项
    }

    override fun areContentsTheSame(oldItem: Memo, newItem: Memo): Boolean {
        return oldItem == newItem // 内容是否完全一致
    }
}

5.3 使用 ListAdapter(推荐)

ListAdapter 内置了 AsyncListDiffer,在后台线程计算差异,使用更简单:

class MemoDiffAdapter(
    private val onItemClick: (Int) -> Unit,
    private val onItemLongClick: (Int) -> Boolean
) : ListAdapter<Memo, MemoViewHolder>(MemoDiffCallback()) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MemoViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.item_memo, parent, false)
        return MemoViewHolder(view).apply {
            itemView.setOnClickListener {
                val pos = adapterPosition
                if (pos != RecyclerView.NO_POSITION) onItemClick(pos)
            }
            itemView.setOnLongClickListener {
                val pos = adapterPosition
                if (pos != RecyclerView.NO_POSITION) onItemLongClick(pos) else false
            }
        }
    }

    override fun onBindViewHolder(holder: MemoViewHolder, position: Int) {
        val memo = getItem(position)
        holder.tvTitle.text = memo.title
        holder.tvTime.text = memo.time
    }
}

更新数据只需一行代码:

adapter.submitList(newList)

DiffUtil 自动在后台计算差异,精准刷新并播放动画。


六、新手必踩坑点清单

坑点 ①:ViewHolder 中使用绝对位置,导致条目错乱

错误写法:把 position 当作常量存起来。

// ❌ 错误!adapterPosition 只在构造时调用一次,不会自动更新
class MemoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    val currentPos = adapterPosition // 固定值,不会变了!
}

正确写法:始终在需要时实时获取 adapterPosition(或 bindingAdapterPosition),并过滤 NO_POSITION。

itemView.setOnClickListener {
    val pos = bindingAdapterPosition
    if (pos != RecyclerView.NO_POSITION) {
        onItemClick(pos)
    }
}

坑点 ②:onBindViewHolder 中遗漏 else 分支,导致内容错位

问题:条目中有条件展示的控件(如重要标记变红),非重要时没有重置颜色,ViewHolder 复用了上一个条目的红色。

错误写法:

override fun onBindViewHolder(holder: MemoViewHolder, position: Int) {
    val memo = memoList[position]
    if (memo.isImportant) {
        holder.tvTitle.setTextColor(Color.RED)
    }
    // ❌ 遗漏 else!非重要 memo 会保留上一个 ViewHolder 的红色
}

正确做法:所有条件判断必须覆盖 else 分支。

if (memo.isImportant) {
    holder.tvTitle.setTextColor(Color.RED)
} else {
    holder.tvTitle.setTextColor(Color.BLACK) // 必须显式还原
}

七、综合小案例:备忘录列表(长按删除 + 动画 + DiffUtil)

综合所学,实现一个功能完善的备忘录列表:

  • 使用 ListAdapter + DiffUtil 实现高性能更新

  • 长按条目弹出确认对话框,确认后删除并播放动画

  • 数据源完全来自内存

7.1 Adapter(ListAdapter 版本)

class MemoAdapter(
    private val onItemClick: (Int) -> Unit,
    private val onItemLongClick: (Int) -> Boolean
) : ListAdapter<Memo, MemoViewHolder>(MemoDiffCallback()) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MemoViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.item_memo, parent, false)
        return MemoViewHolder(view).apply {
            itemView.setOnClickListener {
                val pos = adapterPosition
                if (pos != RecyclerView.NO_POSITION) onItemClick(pos)
            }
            itemView.setOnLongClickListener {
                val pos = adapterPosition
                if (pos != RecyclerView.NO_POSITION) onItemLongClick(pos) else false
            }
        }
    }

    override fun onBindViewHolder(holder: MemoViewHolder, position: Int) {
        val memo = getItem(position)
        holder.tvTitle.text = memo.title
        holder.tvTime.text = memo.time
    }

    class MemoDiffCallback : DiffUtil.ItemCallback<Memo>() {
        override fun areItemsTheSame(oldItem: Memo, newItem: Memo) = oldItem.id == newItem.id
        override fun areContentsTheSame(oldItem: Memo, newItem: Memo) = oldItem == newItem
    }
}

7.2 Activity 完整代码

class MemoActivity : AppCompatActivity() {

    private val memoList = mutableListOf<Memo>()
    private var nextId = 0
    private lateinit var adapter: MemoAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_memo)

        // 加载初始数据
        for (i in 1..15) {
            memoList.add(Memo(++nextId, "学习 RecyclerView 第 $nextId 天", "2025-05-20 10:$nextId"))
        }

        adapter = MemoAdapter(
            onItemClick = { pos ->
                val memo = adapter.currentList[pos]
                Toast.makeText(this, memo.title, Toast.LENGTH_SHORT).show()
            },
            onItemLongClick = { pos ->
                showDeleteDialog(adapter.currentList[pos])
                true
            }
        )

        findViewById<RecyclerView>(R.id.rv_memo).apply {
            layoutManager = LinearLayoutManager(this@MemoActivity)
            adapter = this@MemoActivity.adapter
            itemAnimator = DefaultItemAnimator()
            setHasFixedSize(true)
        }

        adapter.submitList(memoList.toList()) // 提交初始数据

        findViewById<Button>(R.id.btn_add).setOnClickListener { addNewMemo() }
    }

    private fun addNewMemo() {
        val newMemo = Memo(++nextId, "新备忘录 $nextId", "2025-05-20 10:$nextId")
        memoList.add(0, newMemo)
        adapter.submitList(memoList.toList())
        findViewById<RecyclerView>(R.id.rv_memo).scrollToPosition(0)
    }

    private fun showDeleteDialog(memo: Memo) {
        AlertDialog.Builder(this)
            .setTitle("删除确认")
            .setMessage("确定要删除「${memo.title}」吗?")
            .setPositiveButton("删除") { _, _ ->
                memoList.removeAll { it.id == memo.id }
                adapter.submitList(memoList.toList())
                Toast.makeText(this, "已删除", Toast.LENGTH_SHORT).show()
            }
            .setNegativeButton("取消", null)
            .show()
    }
}

7.3 运行效果

  • 列表显示 15 条备忘录,滑动流畅。

  • 点击条目弹出 Toast 提示标题。

  • 长按弹出确认框,确认后条目平滑淡出,列表自动补位。

  • 点击「新增」按钮在顶部插入新条目,带有默认插入动画。

  • 全部更新通过 DiffUtil 精准刷新,无卡顿。


八、总结与下篇预告

今天我们彻底拿下了 Android 列表开发的王者组件 —— RecyclerView,重点梳理:

  • ✅ RecyclerView 优势:四级缓存 + 强制 ViewHolder,布局解耦,性能完胜 ListView。

  • ✅ 四步搭建流程:依赖 → Item 布局 → ViewHolder + Adapter → LayoutManager。

  • ✅ 点击事件:在 ViewHolder 构造时绑定,用 adapterPosition 获取实时位置。

  • ✅ 性能优化:ListAdapter + DiffUtil 实现局部刷新和自动动画,告别 notifyDataSetChanged。

  • ✅ 常见大坑:ViewHolder 位置陷阱、onBindViewHolder 的 if 必须带 else。

  • ✅ 实战案例:备忘录列表,支持长按删除、删除动画、新增条目,完美展示所有知识点。

掌握了 RecyclerView,你就真正入了 Android UI 开发的大门。下篇文章,我们将进入存储领域,学习 Android 本地存储技术(SharedPreferences、文件存储、Room 数据库),让你的数据能够持久保存下来。


✨ 如果本文对你有帮助,欢迎点赞、收藏,让更多零基础的小伙伴少走弯路!