多类型 Item:一个 RecyclerView 显示不同布局

说实话,刚接触 RecyclerView 那会儿,我总觉得一个列表里只能放一种样式的卡片。直到有一次做资讯类 App,产品要求「标题+摘要」和「大图+标题」混着排,我才意识到——原来一个 RecyclerView 能装下完全不同的布局。

这就是我们今天要聊的:多类型 Item。说白了,就是让 RecyclerView 的每个条目,根据自己的数据类型,选择不同的布局文件来渲染。

为什么需要多类型 Item?

你想想看,一个新闻列表里,可能有纯文字标题、图文混排、视频卡片、广告位……如果只用一种布局,要么浪费空间,要么信息展示不全。我个人的习惯是:只要列表里出现两种以上视觉差异明显的卡片,就果断上多类型

举个例子:

  • 标题型:只有一行文字,适合快讯
  • 内容型:标题 + 摘要 + 小图,适合普通文章
  • 大图型:大图 + 标题,适合推荐位

这三种卡片,如果硬塞进同一个布局文件,代码会变得又臭又长。用多类型 Item,每种卡片各管各的,清爽多了。

核心原理:getItemViewType

RecyclerView 的适配器里有个方法叫 getItemViewType()。它的作用很简单:告诉 RecyclerView,当前位置的数据该用哪种布局

我画了一张流程图,帮你理清整个逻辑:

数据列表(List<Any>) getItemViewType() 判断类型 TYPE_TITLE TYPE_CONTENT TYPE_IMAGE TitleViewHolder ContentViewHolder ImageViewHolder

流程其实很直白:数据进来 → 判断类型 → 创建对应的 ViewHolder → 绑定数据。每个 ViewHolder 只管自己的布局,互不干扰。

实战:三步实现多类型 Item

我拿一个简单的资讯列表来演示。假设我们有三种数据类型:

类型 布局 数据类
标题 item_title.xml(一个 TextView) TitleItem
内容 item_content.xml(标题 + 摘要 + 小图) ContentItem
大图 item_image.xml(大图 + 标题) ImageItem

第一步:定义数据类和类型常量

// 数据类
data class TitleItem(val title: String)
data class ContentItem(val title: String, val summary: String, val imageUrl: String)
data class ImageItem(val title: String, val imageUrl: String)

// 类型常量
companion object {
    const val TYPE_TITLE = 0
    const val TYPE_CONTENT = 1
    const val TYPE_IMAGE = 2
}
我的小习惯:类型常量用 Int 而不是枚举,因为 RecyclerView 底层对 Int 的处理更高效。虽然差别微乎其微,但写多了就成习惯了。

第二步:重写 getItemViewType

override fun getItemViewType(position: Int): Int {
    return when (dataList[position]) {
        is TitleItem -> TYPE_TITLE
        is ContentItem -> TYPE_CONTENT
        is ImageItem -> TYPE_IMAGE
        else -> throw IllegalArgumentException("未知类型")
    }
}

这里用 when 配合 is 关键字,Kotlin 的语法糖让代码特别干净。我曾经见过有人用 Java 写一堆 if-else,看着就头疼。

第三步:创建不同的 ViewHolder

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
    return when (viewType) {
        TYPE_TITLE -> {
            val view = LayoutInflater.from(parent.context)
                .inflate(R.layout.item_title, parent, false)
            TitleViewHolder(view)
        }
        TYPE_CONTENT -> {
            val view = LayoutInflater.from(parent.context)
                .inflate(R.layout.item_content, parent, false)
            ContentViewHolder(view)
        }
        TYPE_IMAGE -> {
            val view = LayoutInflater.from(parent.context)
                .inflate(R.layout.item_image, parent, false)
            ImageViewHolder(view)
        }
        else -> throw IllegalArgumentException("未知 viewType: $viewType")
    }
}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
    when (holder) {
        is TitleViewHolder -> holder.bind(dataList[position] as TitleItem)
        is ContentViewHolder -> holder.bind(dataList[position] as ContentItem)
        is ImageViewHolder -> holder.bind(dataList[position] as ImageItem)
    }
}

每个 ViewHolder 内部实现自己的 bind() 方法,各自处理自己的数据绑定逻辑。这样就算以后要加新的卡片类型,只需要新增一个 ViewHolder 和布局文件,完全不影响已有的代码。

注意:onBindViewHolder 里做类型转换时,一定要确保数据类和 ViewHolder 匹配。我曾经因为数据顺序搞错,导致大图数据绑到了标题 ViewHolder 上,直接崩溃。后来我加了一层日志打印,才定位到问题。

避坑指南:我踩过的三个坑

  • 坑一:类型常量重复——如果你有多个适配器,类型常量最好用 companion object 封装,避免不同适配器之间冲突。
  • 坑二:数据类没有基类——我建议所有数据类实现一个密封类或接口,这样 dataList 的类型可以声明为 List<BaseItem>,更安全。
  • 坑三:忘记处理空数据——如果某个类型的数据为空,记得在 getItemViewType 里返回一个占位类型,否则会数组越界。

性能优化:复用才是王道

RecyclerView 之所以高效,靠的就是 ViewHolder 的复用。多类型 Item 并不会破坏复用机制——相同类型的条目会共用同一个 ViewHolder 池。比如列表里有 10 个标题型条目,RecyclerView 只会创建足够显示在屏幕上的那几个 ViewHolder,滚动时循环复用。

所以放心大胆地用多类型,不会影响性能。我做过一个列表,里面混了 6 种卡片类型,滑动依然丝滑。

总结

多类型 Item 的核心就三点:

  1. getItemViewType 告诉 RecyclerView 每个位置该用哪种布局
  2. onCreateViewHolder 里根据类型创建对应的 ViewHolder
  3. onBindViewHolder 里把数据绑定到正确的 ViewHolder 上

掌握了这个模式,你就能轻松应对各种复杂的列表需求。下次产品经理再提「这里要加一种新卡片」,你心里就有底了。


公众号:蓝海资料掘金营,微信deep3321