列表王者 RecyclerView 完全指南 —— 从新手到灵活运用
彻底掌握 RecyclerView,替代 ListView,实现高性能列表开发
哈喽,各位坚持学习的小伙伴们!上一篇我们攻克了 Fragment,现在你的 App 已经可以搭出灵活的页面架构了。但还有一个几乎所有 App 都绕不开的核心组件 —— 列表。
无论是朋友圈的动态流、淘宝的商品列表、还是备忘录的条目,背后都是同一个东西:RecyclerView。它是 Google 官方推出的高性能列表控件,用来取代老旧的 ListView,也是面试必问、开发必会的基础技能。
今天,我们就从零开始,把 RecyclerView 的原理、搭建步骤、点击事件、性能优化、常见踩坑一次性讲透,最后再做一个带删除动画的备忘录实战案例。
全程附 Kotlin 代码,保证新手也能跑起来!
一、RecyclerView 是什么?为什么能淘汰 ListView?
1.1 从 ListView 到 RecyclerView 的进化
ListView 作为 Android 早期的列表组件,存在诸多无法解决的硬伤:
| 对比维度 | ListView | RecyclerView |
|---|---|---|
| 布局能力 | 仅支持垂直线性布局 | 支持线性、网格、瀑布流、横向滑动,可自定义 |
| 缓存机制 | 二级缓存,仅缓存视图 | 四级缓存,缓存视图 + 数据,复用率极高 |
| 解耦设计 | 高度耦合,布局、动画、分割线混在一起 | 完全解耦,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 数据库),让你的数据能够持久保存下来。
✨ 如果本文对你有帮助,欢迎点赞、收藏,让更多零基础的小伙伴少走弯路!