使用RecyclerView显示列表
使用RecyclerView显示列表:保留第四版正文机制,以所有者—状态—结果合同、事件轨迹和章专属故障完成可重放验收。
学习目标
- 能沿“使用RecyclerView显示列表”的用户事件解释Android组件、状态所有者、线程与销毁边界。
- 能围绕“掌握 RecyclerView 的 Adapter-ViewHolder-LayoutManager 三元架构,能实现高效的列表展示、点击处理和条目更新。”改出一个可运行结果,并用前后状态而非组件数量验收。
- 能在旋转、进程重建、拒权、离线或无效输入中选择适用反例,定位首个状态分叉。
- 能用创建/绑定/回收计数、item身份与帧时间独立重放结论,并标明第四版机制与现代targetSdk政策的边界。
为什么需要一个"列表控件"?
几乎每个 App 都有列表:微信的聊天列表、淘宝的商品列表、备忘录的任务列表……这些列表动辄几百条数据,如果一次性把所有 View 都创建出来,手机内存直接爆炸。
Android 的解决方案可以这样理解:你坐在一辆高铁上,窗外能看到的座椅永远只有你所在车厢的那几十个。前面走过、后面还没走的车厢,虽然"存在"但并不占用你的视线。RecyclerView 就遵循同样的原则——它只创建刚好能填满屏幕的那几个列表条目 View,条目滚出屏幕后 View 会被回收复用,给新条目装上新数据。
没有这种"复用"机制会怎样?ListView(Android 早期的列表控件)做了一部分回收,但把"创建 View"和"给 View 绑定数据"两件事揉在一起,导致你很难定制布局、也很难加入动画。RecyclerView 把"创建"、"绑定"、"摆放"拆成三个独立角色,灵活度提升了一个数量级。
RecyclerView 是怎么工作的
三元架构:Adapter、ViewHolder、LayoutManager
RecyclerView 不是一个大坨代码,而是三个零件精密配合的结果:
- ↡RecyclerView.Adapter 负责两件事:在需要时创建 ViewHolder(工厂),在数据变化时给 ViewHolder 塞新数据(填表员)。它是 RecyclerView 和数据源之间的桥梁。 —— 数据源 → View 的桥梁。它知道有多少条数据,知道每条数据长什么样,负责"按需生产"ViewHolder
-
↡RecyclerView.ViewHolder 是一个「座位」,里面放了一个条目的所有子 View 引用(findViewById 的结果),避免反复 findViewById。
—— 条目的"座位"。它缓存一个条目布局里各个子 View 的引用,让你不用每次都
findViewById - ↡RecyclerView.LayoutManager 决定列表怎么摆——是纵向线性列表还是网格,还是瀑布流。它只管「位置和排列」,不管数据。 —— 条目怎么排列。LinearLayoutManager 排成一列,GridLayoutManager 排成网格,StaggeredGridLayoutManager 排成瀑布流
ViewHolder 的"废物利用"机制
当条目滚出屏幕时,RecyclerView 不销毁它的 View,而是把它扔进一个回收池。当新条目要滚进屏幕时,RecyclerView 先看回收池里有没有现成的 ViewHolder——有就拿过来复用,只需要换一下数据就行,不用重新创建 View 和 findViewById。这就是 RecyclerView 能丝滑滚动百条数据的核心秘密。
猜一猜:RecyclerView 在屏幕上一次创建了几个条目?动手算一下:屏幕高度 ÷ 每个条目高度 = 实际创建的 ViewHolder 数量——正是你屏幕能塞下的条目数,不会多也不会少。
Adapter 的三个核心方法,逐一看
每个 Adapter 必须实现三个方法:
| 方法 | 调用时机 | 做什么 |
|---|---|---|
onCreateViewHolder() | 需要新条目 View 时 | 创建 ViewHolder(inflate 布局 + 持有子 View 引用) |
onBindViewHolder() | 条目即将显示时 | 把第 position 条的数据塞给 ViewHolder |
getItemCount() | Adapter 初始化时 | 告诉 RecyclerView 总共多少条 |
① 创建 ViewHolder 类
定义一个继承 RecyclerView.ViewHolder 的类,在构造器中缓存条目布局里各子 View 的引用(findViewById 结果)。这些引用在条目复用时直接取值,不再需要重复查找——ViewHolder 就是条目的"座位"。
代码逐段拆解
第一步:定义布局和 ViewHolder
class CrimeHolder(view: View) : RecyclerView.ViewHolder(view) {
val titleTextView: TextView = view.findViewById(R.id.crime_title)
val dateTextView: TextView = view.findViewById(R.id.crime_date)
fun bind(crime: Crime) {
titleTextView.text = crime.title
dateTextView.text = crime.date.toString()
}
}ViewHolder 构造器里传 View(由 onCreateViewHolder inflate 出来的那一行),然后它把各个子 View 的引用缓存起来——val 而非 var,因为引用本身不变,变的只是引用指向的 View 上的内容。
第二步:编写 Adapter
class CrimeAdapter(
private val crimes: List<Crime>,
private val onItemClick: (Crime) -> Unit
) : RecyclerView.Adapter<CrimeHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CrimeHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.list_item_crime, parent, false)
return CrimeHolder(view)
}
override fun onBindViewHolder(holder: CrimeHolder, position: Int) {
val crime = crimes[position]
holder.bind(crime)
holder.itemView.setOnClickListener { onItemClick(crime) }
}
override fun getItemCount(): Int = crimes.size
}onCreateViewHolder 只在需要新 View 时才调用(回收池空的时候)。onBindViewHolder 每显示一条就调一次,把 crimes[position] 的数据塞进 ViewHolder。点击处理用 lambda 传出去——比内部接口更简洁。
第三步:绑定 RecyclerView 到 Activity
class CrimeListActivity : AppCompatActivity() {
private lateinit var recyclerView: RecyclerView
private val adapter = CrimeAdapter(emptyList()) { crime ->
// 处理点击:跳转详情页
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_crime_list)
recyclerView = findViewById(R.id.recycler_view)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = adapter
}
fun updateUI(crimes: List<Crime>) {
adapter.updateData(crimes)
}
}第四步:ListAdapter——更现代的选择
如果你的列表数据经常变(增删改),建议用 ListAdapter(继承自 RecyclerView.Adapter),它内置了 DiffUtil 自动计算差异:
class CrimeListAdapter(
private val onItemClick: (Crime) -> Unit
) : ListAdapter<Crime, CrimeHolder>(DiffCallback) {
companion object DiffCallback : DiffUtil.ItemCallback<Crime>() {
override fun areItemsTheSame(oldItem: Crime, newItem: Crime): Boolean =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Crime, newItem: Crime): Boolean =
oldItem == newItem
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CrimeHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.list_item_crime, parent, false)
return CrimeHolder(view)
}
override fun onBindViewHolder(holder: CrimeHolder, position: Int) {
holder.bind(getItem(position))
holder.itemView.setOnClickListener { onItemClick(getItem(position)) }
}
}使用 ListAdapter 后,更新数据只需 adapter.submitList(newList),它会自动计算新旧列表的差异,只刷新真正变化的条目——滚动位置不会跳,动画也更流畅。
容易踩的坑
小结
- RecyclerView 把列表拆成三件独立的事情:Adapter(管数据)、ViewHolder(管 View 复用)、LayoutManager(管排列)
- ViewHolder 缓存子 View 引用 + 回收池复用 = 流畅滚动上百条数据的核心
onCreateViewHolder创建 ViewHolder(少调);onBindViewHolder绑定数据(每条调)- 简单场景用
RecyclerView.Adapter;频繁增删改的数据用ListAdapter+DiffUtil
练习
问题 1:“第9章 Displaying Lists with RecyclerView”覆盖哪些正式节点和项目主线?
问题 2:怎样建立本页最小可执行实验?
问题 3:为什么只在正常点击路径运行不能证明完成?
问题 4:怎样设计能推翻当前实现的反例?
问题 5:从第4版语境迁移到现代目标SDK时如何控制变量?
问题 6:本页达到独立交接标准需要什么?
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- Adapter
数据到 View 的桥梁。它告诉 RecyclerView 有多少条数据、每条数据对应的 View 长什么样。类比餐厅:Adapter 是服务员——厨房(数据源)做好菜,服务员端到桌上(屏幕)。
- ViewHolder
一个条目的 View 引用仓库。它把条目布局里的各个子 View 缓存起来,避免每次都要全屏
findViewById。类比高铁座椅——人换了(数据变了),但座椅本身(ViewHolder)可以复用。- LayoutManager
决定列表条目怎么排列的角色。LinearLayoutManager 是竖排或横排线性列表,GridLayoutManager 是网格,StaggeredGridLayoutManager 是不等高的瀑布流。三个可互换,不影响 Adapter 的代码。
- DiffUtil
比较新旧两个列表的"差异计算器"。算出哪些条目是新增的、哪些被删了、哪些内容变了,让 RecyclerView 只刷新真正变化的条目而不是整屏重建。
“使用RecyclerView显示列表”不使用未获授权的纸书正文;InformIT出版信息与授权电子版完整目录只用于确认第四版32章、269个正式目录节点和时代语境,第四版官方勘误用于识别工具链变更。下列中文解释、图示、交互、代码与练习均为独立教学重写,平台行为再以Android Developers的一手文档复核。
为什么“使用RecyclerView显示列表”必须回到可观察状态
“使用RecyclerView显示列表”的学习结果不是记住类名,而是能预测“掌握 RecyclerView 的 Adapter-ViewHolder-LayoutManager 三元架构,能实现高效的列表展示、点击处理和条目更新。”在一次输入、一次重建和一次失败中的不同状态,并指出哪条Android合同产生差异。
第四版机制逐项深读
9. Displaying Lists with RecyclerView
在“使用RecyclerView显示列表”中,验证“9. Displaying Lists with RecyclerView”用插入、删除、重排和多类型数据检查item身份、焦点与点击目标,记录首个错误绑定。
Adding a New Fragment and ViewModel
在“使用RecyclerView显示列表”中,分析“Adding a New Fragment and ViewModel”要画出事件进入、状态归约、数据源写入和UI重绘四条边,并指出配置变化时谁被重建。
Adding a RecyclerView
在“使用RecyclerView显示列表”中,“Adding a RecyclerView”只为可见项创建和绑定ViewHolder,回收时旧状态必须被新数据完整覆盖;稳定标识不能用易变位置冒充。
Creating an Item View Layout
在“使用RecyclerView显示列表”中,“Creating an Item View Layout”把约束、测量与布局落实为View树几何;用小屏、长文本与大字体验证,不能用单一模拟器截图证明适配。
Implementing a ViewHolder
在“使用RecyclerView显示列表”中,“Implementing a ViewHolder”的性能由帧时间、绑定分配与diff范围共同决定,不以单次滑动的主观流畅度验收。
Implementing an Adapter to Populate the RecyclerView
在“使用RecyclerView显示列表”中,“Implementing an Adapter to Populate the RecyclerView”的性能由帧时间、绑定分配与diff范围共同决定,不以单次滑动的主观流畅度验收。
Recycling Views
在“使用RecyclerView显示列表”中,验证“Recycling Views”用插入、删除、重排和多类型数据检查item身份、焦点与点击目标,记录首个错误绑定。
Cleaning Up Binding List Items
在“使用RecyclerView显示列表”中,“Cleaning Up Binding List Items”若依赖第四版时期API,应分开说明原书机制与现代平台政策,并用Android官方文档核对迁移边界。
Responding to Presses
在“使用RecyclerView显示列表”中,分析“Responding to Presses”先冻结设备API、targetSdk和输入,再沿回调与数据流寻找首个状态分叉,不能只描述最终页面。
For the More Curious: ListView and GridView
在“使用RecyclerView显示列表”中,验证“For the More Curious: ListView and GridView”用插入、删除、重排和多类型数据检查item身份、焦点与点击目标,记录首个错误绑定。
Challenge: RecyclerView ViewTypes
在“使用RecyclerView显示列表”中,分析“Challenge: RecyclerView ViewTypes”分别统计创建、绑定、回收和差量更新,列表能滚动并不证明没有错位或全量重绘。
“使用RecyclerView显示列表”验收回顾
“使用RecyclerView显示列表”只有在创建/绑定/回收计数、item身份与帧时间能够从相同基线再次得到相同断言时才通过;第四版机制与现代平台政策分别记录,不用新API名称掩盖旧行为。
← 上一页:UI fragment与fragment管理器 · 下一页:使用布局与部件创建用户界面 →
章专属可重放状态实验
先预测“滚动复用、插入、删除、点击和数据刷新”发生后,Adapter 数据快照与每个 ViewHolder应怎样改变item ID、类型、绑定内容、选择与可见位置;再操作三个实验。第四版示例与当前 Android 政策分别记录,实验不把新 API 名称倒填为原书内容。
实验一:所有者—状态—结果合同
选择任一正式目录节点和正常/边界场景,检查它是否真的进入本章状态合同。目录标题只有同时出现在解释、可视状态和交付证据中才算覆盖。
Owner · state · observable result
使用RecyclerView显示列表:状态合同
让 Adapter 以稳定事实完整绑定复用 ViewHolder,而非依赖旧位置状态
验证场景
第四版正式目录节点
recyclerview · 正常任务
9. Displaying Lists with RecyclerView:固定 SDK、设备配置和初始状态,触发“滚动复用、插入、删除、点击和数据刷新”
冻结入口:9. Displaying Lists with RecyclerView
记录Adapter 数据快照与每个 ViewHolder的初始item ID、类型、绑定内容、选择与可见位置
观察:绑定日志、holder ID、item ID、diff 结果与滚动断言中的“9. Displaying Lists with RecyclerView”轨迹
预期:由Adapter 数据快照与每个 ViewHolder提交item ID、类型、绑定内容、选择与可见位置,并持续满足“每次 bind 覆盖全部可变视图,点击按当前 item 身份解释”
实验二:事件与生命周期轨迹
沿五次转换逐步执行“滚动复用、插入、删除、点击和数据刷新”。每一步只允许Adapter 数据快照与每个 ViewHolder按职责提交状态,并持续核对“每次 bind 覆盖全部可变视图,点击按当前 item 身份解释”。
Deterministic event replay
使用RecyclerView显示列表:事件轨迹
不变量:每次 bind 覆盖全部可变视图,点击按当前 item 身份解释
交付证据:绑定日志、holder ID、item ID、diff 结果与滚动断言
实验三:章专属反例与同输入恢复
注入“复用后未重置 checkbox,上一行的选中状态泄漏到新数据”,保存第一个偏离点;撤销后以完全相同的 SDK、设备状态和用户事件重放。只有绑定日志、holder ID、item ID、diff 结果与滚动断言一起恢复才算修复。
Fault · cancel · restore
使用RecyclerView显示列表:反例与恢复
故障:复用后未重置 checkbox,上一行的选中状态泄漏到新数据
第 1 次使用相同 SDK、设备配置、初始状态与用户事件
保持正常输入不变,仅注入“复用后未重置 checkbox,上一行的选中状态泄漏到新数据”
每次 bind 覆盖全部可变视图,点击按当前 item 身份解释
绑定日志、holder ID、item ID、diff 结果与滚动断言