使用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 不是一个大坨代码,而是三个零件精密配合的结果:

  • —— 数据源 → View 的桥梁。它知道有多少条数据,知道每条数据长什么样,负责"按需生产"ViewHolder
  • —— 条目的"座位"。它缓存一个条目布局里各个子 View 的引用,让你不用每次都 findViewById
  • —— 条目怎么排列。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 总共多少条
分步1 / 5

① 创建 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、设备配置和初始状态,触发“滚动复用、插入、删除、点击和数据刷新”

状态所有者Adapter 数据快照与每个 ViewHolder
受控状态item ID、类型、绑定内容、选择与可见位置
触发事件滚动复用、插入、删除、点击和数据刷新

冻结入口: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显示列表:事件轨迹

选择一次状态转换1 / 5

不变量:每次 bind 覆盖全部可变视图,点击按当前 item 身份解释

交付证据:绑定日志、holder ID、item ID、diff 结果与滚动断言

实验三:章专属反例与同输入恢复

注入“复用后未重置 checkbox,上一行的选中状态泄漏到新数据”,保存第一个偏离点;撤销后以完全相同的 SDK、设备状态和用户事件重放。只有绑定日志、holder ID、item ID、diff 结果与滚动断言一起恢复才算修复。

Fault · cancel · restore

使用RecyclerView显示列表:反例与恢复

故障:复用后未重置 checkbox,上一行的选中状态泄漏到新数据

1. 冻结输入一致

第 1 次使用相同 SDK、设备配置、初始状态与用户事件

2. 注入边界一致

保持正常输入不变,仅注入“复用后未重置 checkbox,上一行的选中状态泄漏到新数据”

3. 检查所有者一致

每次 bind 覆盖全部可变视图,点击按当前 item 身份解释

4. 核对结果一致

绑定日志、holder ID、item ID、diff 结果与滚动断言

讨论

评论区加载中…