Android 笔记:RecyclerView

RecyclerView 很适合用于数据集合的展示,包括以列表形式、网格形式等。它的特点是:只会维持当前界面上能够显示的数量的视图实例。例如一个屏幕只显示得下 10 条数据,那么它就只会在内存中保留稍多于 10 个的视图实例。当界面滚动时,部分数据的界面被隐藏,相应的视图实例会被回收重用于新数据的显示,这也就是它名字的由来:Recycler。这可以极大地节省系统资源。

引入 RecyclerView

# lib.versions.toml

[versions]  
recyclerview = "1.3.2"  
  
[libraries]  
androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" }  
// build.gradle.kts

dependencies {  
    implementation(libs.androidx.recyclerview)
}

使用 RecyclerView

创建好 Fragment 或 Activity,在布局文件中使用 androidx.recyclerview.widget.RecyclerView

<?xml version="1.0" encoding="utf-8"?>  
<androidx.recyclerview.widget.RecyclerView  
    xmlns:android="http://schemas.android.com/apk/res/android"  
    android:id="@+id/crime_recycler_view"  
    android:layout_width="match_parent"  
    android:layout_height="match_parent"/>

RecyclerView 本身是个容器,只负责管理其中的子项,通过回收和重用子项来提高性能,避免频繁创建和销毁视图。

要使用 RecyclerView 时,还涉及到几个概念:

LayoutManager

LayoutManager 决定 RecyclerView 的子项如何布局和排列。常见的有:

  • LinearLayoutManager:用于线性排列子项(水平或垂直方向)。
  • GridLayoutManager:用于网格排列子项,可以设置列或行的数量。
  • StaggeredGridLayoutManager:用于不规则的网格布局,例如瀑布流效果。

还可以自定义 LayoutManager,根据需求编写自己的布局逻辑。

简单地使用 LinearLayoutManager:

override fun onCreateView(  
    inflater: LayoutInflater, container: ViewGroup?,  
    savedInstanceState: Bundle?  
): View {  
    // ...
    binding.crimeRecyclerView.layoutManager = LinearLayoutManager(context)  
    return binding.root  
}

子项视图

子项是 RecyclerView 中的每一项(例如 CrimeList 中的单个 Crime),对应的视图需要单独定义:

<!-- list_item_crime.xml -->

<?xml version="1.0" encoding="utf-8"?>  
<LinearLayout  
    xmlns:android="http://schemas.android.com/apk/res/android"  
    android:layout_height="wrap_content"  
    android:layout_width="match_parent"  
    android:orientation="vertical"  
    android:padding="8dp">  
    <TextView
        android:id="@+id/crime_title"  
        android:layout_width="match_parent"  
        android:layout_height="wrap_content"  
        android:text="Crime Title"/>  
    <TextView
        android:id="@+id/crime_date"  
        android:layout_width="match_parent"  
        android:layout_height="wrap_content"  
        android:text="Date"/>  
</LinearLayout>

ViewHolder

ViewHolder 持有子项视图的引用,RecyclerView 在内部通过 ViewHolder 的实例间接操作子项视图,实现与子项视图的交互和管理。

class CrimeHolder(val binding: ListItemCrimeBinding) : RecyclerView.ViewHolder(binding.root) { 
}

自定义 ViewHolder 类继承自 RecyclerView.ViewHolder。构造方法这里使用了 view binding 的实例作为参数,同时将根视图传入父类的构造方法。

Adapter

RecyclerView 并不是直接创建 ViewHolder 的,而是通过 Adapter 来创建。Adapter 通常维护着整个数据集合。自定义的 Adapter 需要继承 RecyclerView.Adapter,需要实现的有 3 个方法:

  • onCreateViewHolder:创建并返回一个新的 ViewHolder 实例。
  • onBindViewHolder:将对应的数据绑定到指定 ViewHolder。
  • getItemCount:返回数据集合的数量。
class CrimeListAdapter(private val crimes: List<Crime>) : RecyclerView.Adapter<CrimeHolder>() {  
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CrimeHolder {  
        val inflater = LayoutInflater.from(parent.context)  
        val binding = ListItemCrimeBinding.inflate(inflater, parent, false)  
        return CrimeHolder(binding)  
    }  
  
    override fun getItemCount() = crimes.size  
  
    override fun onBindViewHolder(holder: CrimeHolder, position: Int) {  
        val crime = crimes[position]  
        holder.binding.crimeTitle.text = crime.title  
        holder.binding.crimeDate.text = crime.date.toString()  
    }  
}

最后,在使用 RecyclerView 的 Fragment 或者 Activity 上给对应的 RecyclerView 的 adapter 赋值:

override fun onCreateView(  
    inflater: LayoutInflater, container: ViewGroup?,  
    savedInstanceState: Bundle?  
): View {  
    _binding = FragmentCrimeListBinding.inflate(inflater, container, false)  
    binding.crimeRecyclerView.layoutManager = LinearLayoutManager(context)  
    binding.crimeRecyclerView.adapter = CrimeListAdapter(crimeListViewModel.crimes)  
    return binding.root  
}

因为 RecyclerView 回收重用的特性,Adapter 的 onCreateViewHolder() 被调用的次数会明显少于 onBindViewHolder()

viewType

如果列表的子项视图会根据对应的数据不同而有所不同,可以利用 onCreateViewHolder(parent: ViewGroup, viewType: Int)viewType 参数来创建不同类型的 ViewHolder 来达到渲染不同视图的目的。而这里的 viewType 来源于 getItemViewType(position) 的返回值,getItemViewType(position) 需要被重写。

override fun getItemViewType(position: Int): Int {
    return when (dataList[position]) {
        is Banner -> VIEW_TYPE_BANNER
        is Product -> VIEW_TYPE_PRODUCT
        is Ad -> VIEW_TYPE_AD
        else -> throw IllegalArgumentException("Invalid type")
    }
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
    return when (viewType) {
        VIEW_TYPE_BANNER -> BannerViewHolder(
            LayoutInflater.from(parent.context).inflate(R.layout.item_banner, parent, false)
        )
        VIEW_TYPE_PRODUCT -> ProductViewHolder(
            LayoutInflater.from(parent.context).inflate(R.layout.item_product, parent, false)
        )
        VIEW_TYPE_AD -> AdViewHolder(
            LayoutInflater.from(parent.context).inflate(R.layout.item_ad, parent, false)
        )
        else -> throw IllegalArgumentException("Invalid view type")
    }
}

事件绑定

因为 ViewHolder 管理着子项视图,因此对于子项视图的事件处理逻辑,可以在 ViewHolder 中编写。首先,把数据绑定的代码从 Adapter 的 onBindViewHolder() 中移到 ViewHolder 里:

class CrimeHolder(private val binding: ListItemCrimeBinding) : RecyclerView.ViewHolder(binding.root) {  
    fun bind(crime: Crime) {  
        binding.crimeTitle.text = crime.title  
        binding.crimeDate.text = crime.date.toString()  
    }  
}

class CrimeListAdapter(private val crimes: List<Crime>) : RecyclerView.Adapter<CrimeHolder>() {  
    override fun onBindViewHolder(holder: CrimeHolder, position: Int) {
        // val crime = crimes[position]  
        // holder.binding.crimeTitle.text = crime.title  
        // holder.binding.crimeDate.text = crime.date.toString()  
        holder.bind(crimes[position])  
    }
}

然后再在 CrimeHolder.bind() 方法里加上事件绑定的代码:

class CrimeHolder(private val binding: ListItemCrimeBinding) : RecyclerView.ViewHolder(binding.root) {  
    fun bind(crime: Crime) {
        // ...
        binding.root.setOnClickListener { _ ->  
	        Toast.makeText(binding.root.context, "${crime.title} clicked!", Toast.LENGTH_SHORT).show()  
	    }
    }  
}

用 ListAdapter 提升性能

Adapter 提供了一些方法用于将数据发生了怎样的变化告诉视图,例如 notifyItemMoved()notifyItemInserted()notifyItemChanged() 等,让界面作出相应的变化。但使用这些方法的前提是知道数据发生了怎样的变化。但在许多情况下,数据如何改变是无法获知的。

如果获取到的是一个修改后的数据副本,要么得通过计算和比较,得出哪些数据发生了变化,然后使用对应的 notify... 方法;要么干脆不计算,直接使用 notifyDataSetChanged() 重新渲染整个列表。当然,后者相对来说,性能和效率要差得多,它无法做到像 notifyItem... 系列方法那样精准地只修改变化了的数据所对应的视图。

这时,如果有方法可以方便地计算出具体是哪些数据发生了变化,就可以使用 notifyItem... 系列方法提高渲染效率了。而 ListAdapter 就提供了这些方法。

ListAdapter 继承自 RecyclerView.Adapter,它内部使用了一个 DiffUtil 类,来检测哪些数据发生了变化。要用它的话,得创建一个类,继承 DiffUtil.ItemCallback,并实现 areItemsTheSame()areContentsTheSame() 方法,再将这个新创建的类的实例传给 ListAdapter 的构造方法。最后通过 ListAdapter.submitList(...) 来通知视图作出变化。

data class MyItem(val id: Int, val name: String, val description: String)

// 定义如何比较两个 MyItem 对象
class MyItemDiffCallback : DiffUtil.ItemCallback<MyItem>() {
    override fun areItemsTheSame(oldItem: MyItem, newItem: MyItem): Boolean {
        // 比较唯一标识符(通常是 ID)
        return oldItem.id == newItem.id
    }

    override fun areContentsTheSame(oldItem: MyItem, newItem: MyItem): Boolean {
        // 比较内容是否完全相同
        return oldItem == newItem
    }
}

// 使用 ListAdapter
class MyAdapter : ListAdapter<MyItem, MyViewHolder>(MyItemDiffCallback()) {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        // 创建 ViewHolder
    }

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        // 绑定数据
        holder.bind(getItem(position))
    }
}
1