说实话,RecyclerView 是 Android 开发里最让人又爱又恨的组件之一。爱的是它强大、灵活,恨的是一旦没玩好,界面卡顿得像 PPT,内存泄漏报告能让你在 Code Review 时社死。今天咱们不整那些虚头巴脑的概念,直接上代码、上实战,把那些坑一个个填平。
一、从零开始:别急着写代码,先想清楚结构
很多新手拿到需求就开始 new RecyclerView(),然后报错、调试、再报错。其实,先画结构图比写代码快三倍。
一个标准的 RecyclerView 三件套:
- RecyclerView:展示列表的容器。
- ViewHolder:代表每一行数据的视图对象,负责绑定数据。
- Adapter:连接数据和视图的桥梁。
1.1 布局文件:简洁才是王道
假设我们要做一个简易的“待办事项列表”。先写 item 布局:
<!-- item_todo.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="56dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:padding="16dp">
<!-- 复用 ID 很重要,避免重复查找 -->
<ImageView
android:id="@+id/iv_todo_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_check" />
<TextView
android:id="@+id/tv_todo_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="12dp"
android:textSize="16sp"
android:textColor="#333333" />
<TextView
android:id="@+id/tv_todo_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12sp"
android:textColor="#999999" />
</LinearLayout>
主布局更简单:
<!-- activity_todo.xml -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_todo_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="16dp" />
关键细节:
clipToPadding="false"让你可以在 RecyclerView 内部加 padding 而不影响滚动区域,体验更好。
1.2 ViewHolder:别再 findViewById 了
很多老代码里,Adapter 的 onCreateViewHolder 里还在 inflate 之后紧接着 findViewById,这是严重性能隐患。ViewHolder 的精髓就是缓存 View 引用。
class TodoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
// 在构造时一次性绑定,后续只操作这些引用
val icon: ImageView = itemView.iv_todo_icon
val content: TextView = itemView.tv_todo_content
val time: TextView = itemView.tv_todo_time
}
1.3 Adapter:核心逻辑在这里
class TodoAdapter(
private val todos: List<TodoItem>
) : RecyclerView.Adapter<TodoViewHolder>() {
// 1. 告诉 RecyclerView 有多少条数据
override fun getItemCount() = todos.size
// 2. 创建 ViewHolder
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_todo, parent, false)
return TodoViewHolder(view)
}
// 3. 绑定数据——这是渲染的核心
override fun onBindViewHolder(holder: TodoViewHolder, position: Int) {
val todo = todos[position]
holder.content.text = todo.content
holder.time.text = todo.formattedTime
// 根据状态设置图标
holder.icon.setImageResource(
if (todo.isCompleted) R.drawable.ic_completed
else R.drawable.ic_pending
)
}
}
在 Activity/Fragment 中初始化:
class TodoActivity : AppCompatActivity() {
private lateinit var binding: ActivityTodoBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityTodoBinding.inflate(layoutInflater)
setContentView(binding.root)
// 初始数据
val sampleData = generateMockTodos()
binding.rvTodoList.apply {
// 必须设置 LayoutManager,否则 RecyclerView 不知道如何排列
layoutManager = LinearLayoutManager(this@TodoActivity)
adapter = TodoAdapter(sampleData)
// 添加分隔线(可选)
addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
}
}
}
到这里,你的列表已经能跑了。但等等,这只是“能跑”,离“好用”还差得远。
二、性能优化:让滚动如丝般顺滑
用户最讨厌的就是列表滑动卡顿。以下是几个实战中验证过的优化点。
2.1 使用 DiffUtil 替代 notifyDatasetChanged
notifyDataSetChanged() 是最简单的刷新方式,但它会重绘所有可见 item,哪怕只改了一个字。对于长列表,这是灾难。
DiffUtil 是 Google 提供的工具类,它能计算出最小变更集,只更新真正变化的部分。
class TodoDiffCallback(
private val oldList: List<TodoItem>,
private val newList: List<TodoItem>
) : DiffUtil.Callback() {
override fun getOldListSize() = oldList.size
override fun getNewListSize() = newList.size
// 判断两个 item 是否是同一个
override fun areItemsTheSame(oldPos: Int, newPos: Int): Boolean {
return oldList[oldPos].id == newList[newPos].id
}
// 判断两个 item 的内容是否相同
override fun areContentsTheSame(oldPos: Int, newPos: Int): Boolean {
return oldList[oldPos] == newList[newPos]
}
}
class TodoAdapter(
private var todos: MutableList<TodoItem> = mutableListOf()
) : RecyclerView.Adapter<TodoViewHolder>() {
// ... 前面的代码不变 ...
// 刷新方法
fun submitList(newList: List<TodoItem>) {
val diffCallback = TodoDiffCallback(todos, newList)
val diffResult = DiffUtil.calculateDiff(diffCallback)
todos.clear()
todos.addAll(newList)
diffResult.dispatchUpdatesTo(this)
}
}
调用时:
// 假设你从网络或数据库拿到了新数据
val newTodos = fetchTodosFromApi()
adapter.submitList(newTodos)
好处:RecyclerView 会自动计算哪些 item 位置变了、内容变了、是增删,然后只动画更新变化的部分。性能提升显著。
2.2 启用 ViewHoler 缓存优化
如果你有多段不同类型的 item(比如标题栏 + 内容项),可以用 RecyclerView.RecycledViewPool 共享缓存池。
class MultiTypeAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
companion object {
private const val TYPE_HEADER = 1
private const val TYPE_ITEM = 2
}
override fun getItemViewType(position: Int): Int {
return if (position == 0) TYPE_HEADER else TYPE_ITEM
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return if (viewType == TYPE_HEADER) {
// 头部 View
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_header, parent, false)
HeaderViewHolder(view)
} else {
// 普通 item,复用同一个 ViewHolder
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_todo, parent, false)
TodoViewHolder(view)
}
}
// 对于大量复用相同 ViewHolder 类型的场景,可以配置 RecycledViewPool
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
super.onAttachedToRecyclerView(recyclerView)
val pool = recyclerView.recycledViewPool
pool.setMaxRecycledViews(TodoViewHolder::class.java, 5) // 最多缓存 5 个
}
}
2.3 图片加载优化
列表里如果每张图都用 ImageView.setImageResource() 加载大图,内存直接爆炸。务必使用 Glide、Coil 或 Fresco。
// 在 onBindViewHolder 中
Glide.with(holder.icon.context)
.load(todo.imageUrl)
.placeholder(R.drawable.ic_loading)
.error(R.drawable.ic_error)
.into(holder.icon)
注意:Glide 会自动处理绑定到 ViewHolder 的生命周期,不会导致内存泄漏(后面会详细讲)。
2.4 预加载与分页
当数据量大时(1000+ 条),一次性加载全部数据到内存是不现实的。使用 Paging 3 库是分页的最佳实践。
// ViewModel 中
val todoPagingData: PagingData<TodoItem> = pagerFlow
// Activity 中
class TodoActivity : AppCompatActivity() {
private lateinit var adapter: PagingDataAdapter<TodoItem, TodoViewHolder>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 使用 PagedListAdapter,内部已集成 DiffUtil
adapter = PagedListAdapter<TodoItem, TodoViewHolder>(TodoDiffCallback()) {
// 绑定逻辑
}
binding.rvTodoList.apply {
layoutManager = LinearLayoutManager(this@TodoActivity)
adapter = this@TodoActivity.adapter
}
// 收集 PagingData 并更新适配器
lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
todoViewModel.todoPagingData.collect { pagingData ->
adapter.submitData(pagingData)
}
}
}
}
}
Paging 3 会自动懒加载数据,用户滚动到哪里才请求哪里,极大节省内存和网络流量。
三、内存泄漏:那些隐蔽的坑
内存泄漏是 Android 开发的经典难题。RecyclerView 相关的泄漏点主要有以下几个。
3.1 ViewHolder 中持有 Context 导致 Activity 泄漏
这是最常见的错误之一。
// ❌ 错误示范:ViewHolder 持有 Activity Context
class BadViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val context = itemView.context // 隐式持有 Activity
fun loadData() {
// 这里如果启动了一个异步任务,或者注册了 BroadcastReceiver
// 即使 Activity 销毁了,这个 ViewHolder 还活着,导致 Activity 无法被 GC
val task = object : AsyncTask<Void, Void, String>() {
override fun onPostExecute(result: String?) {
// 使用 context 显示 Toast
Toast.makeText(context, result, Toast.LENGTH_SHORT).show()
}
}
task.execute()
}
}
修复方案:使用 Application Context,或者在 Activity 销毁时取消任务。
// ✅ 正确做法:使用 Application Context
class GoodViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val appContext = itemView.context.applicationContext
fun loadData() {
val task = object : AsyncTask<Void, Void, String>() {
override fun onPostExecute(result: String?) {
Toast.makeText(appContext, result, Toast.LENGTH_SHORT).show()
}
}
task.execute()
}
}
3.2 在 Adapter 中注册未取消的监听器
如果你在 Adapter 里给 View 注册了点击监听,或者给 RecyclerView 注册了滚动监听,但没有在 onDetachedFromRecyclerView 中取消,就会造成泄漏。
class LeakyAdapter : RecyclerView.Adapter<ViewHolder>() {
private val scrollCallback = object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
// 这里如果引用了外部对象...
}
}
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
super.onAttachedToRecyclerView(recyclerView)
recyclerView.addOnScrollListener(scrollCallback)
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
super.onDetachedFromRecyclerView(recyclerView)
// ⚠️ 必须取消注册!否则 Adapter 持有 RecyclerView,RecyclerView 持有 Adapter,形成循环引用
recyclerView.removeOnScrollListener(scrollCallback)
}
}
3.3 懒加载图片时未取消请求
虽然 Glide 会自动处理生命周期,但如果你用 Glide 的 RequestManager 绑定到 Activity,而 ViewHolder 的生命周期长于 Activity,就可能有问题。
更好的做法是使用 Glide 的 RequestManager 绑定到 Application Context,或者使用 Coil,它对 Lifecycle-aware 的支持更原生。
// 使用 Coil,生命周期自动感知
Coil.imageLoader(context.applicationContext) // 使用 Application Context
.load(todo.imageUrl) {
crossfade(true)
placeholder(R.drawable.ic_loading)
}
.into(holder.icon)
3.4 在 onBindViewHolder 中启动异步任务
// ❌ 危险操作
override fun onBindViewHolder(holder: TodoViewHolder, position: Int) {
val todo = todos[position]
// 如果这个 ViewHolder 被回收后复用,但之前的异步任务还在运行
// 它可能会更新已经不属于这个 position 的 View
loadThumbnail(todo.id) { image ->
holder.icon.setImageDrawable(image)
}
}
修复:使用 Glide/Coil 自动取消旧请求,或者在 ViewHolder 中检查 bindingAdapterPosition 是否有效。
// ✅ 安全做法
override fun onBindViewHolder(holder: TodoViewHolder, position: Int) {
if (holder.bindingAdapterPosition == RecyclerView.NO_POSITION) return
val todo = todos[position]
// 使用 Glide,它会自动取消该 ImageView 上之前的请求
Glide.with(holder.icon.context)
.load(todo.imageUrl)
.into(holder.icon)
}
四、高级技巧:增强用户体验
4.1 添加 Item 动画
RecyclerView 默认支持一些动画,但你可以自定义。
class TodoAdapter(...) : RecyclerView.Adapter<TodoViewHolder>() {
override fun onBindViewHolder(holder: TodoViewHolder, position: Int) {
// ... 绑定数据 ...
// 添加进入动画
holder.itemView.alpha = 0f
holder.itemView.translationY = 50f
holder.itemView.animate()
.alpha(1f)
.translationY(0f)
.setDuration(300)
.start()
}
}
4.2 处理快速滑动时的图片闪烁
快速滑动列表时,如果图片加载慢,会出现“图片闪烁”或“错乱”。使用 Glide 的 dontAnimate() 或设置 override(size) 可以避免:
Glide.with(context)
.load(url)
.override(200, 200) // 指定固定尺寸,避免图片缩放计算开销
.diskCacheStrategy(DiskCacheStrategy.DATA) // 缓存原图和数据
.into(imageView)
4.3 使用 ViewBinding 替代 findViewById
如果你还在用 findViewById,真的该升级了。ViewBinding 在编译期生成绑定类,类型安全,且零开销。
// 生成绑定类
class TodoViewHolder(private val binding: ItemTodoBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(todo: TodoItem) {
binding.tvContent.text = todo.content
binding.tvTime.text = todo.formattedTime
// Glide 绑定到 binding.ivIcon
Glide.with(binding.ivIcon.context)
.load(todo.imageUrl)
.into(binding.ivIcon)
}
}
// 在 Adapter 中创建
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoViewHolder {
val binding = ItemTodoBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return TodoViewHolder(binding)
}
五、完整示例:一个生产级别的 Todo 列表
把上面的所有最佳实践整合在一起:
”`kotlin // TodoItem.kt data class TodoItem(
val id: String,
val content: String,
val isCompleted: Boolean,
val timestamp: Long
) {
val formattedTime: String
get() = SimpleDateFormat("MM-dd HH:mm").format(Date(timestamp))
}
// TodoDiffCallback.kt
class TodoDiffCallback : DiffUtil.ItemCallback
override fun areItemsTheSame(oldItem: TodoItem, newItem: TodoItem): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: TodoItem, newItem: TodoItem): Boolean {
return oldItem == newItem
}
}
// TodoAdapter.kt
class TodoAdapter : PagedListAdapter
class TodoViewHolder(private val binding: ItemTodoBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: TodoItem) {
binding.tvContent.text = item.content
binding.tvTime.text = item.formattedTime
binding.ivIcon.setImageResource(
if (item.isCompleted) R.drawable.ic_completed else R.drawable.ic_pending
)
// 点击事件
binding.root.setOnClickListener {
// 通过 Lambda 回调,避免在 ViewHolder 中直接持有 Activity 引用
onItemClicked?.invoke(item)
}
}
}
var onItemClicked: ((TodoItem) -> Unit)?
