Android UI 组件系列(十一):RecyclerView 多类型布局与数据刷新实战
博客专栏:Android初级入门UI组件与布局
源码:通过网盘分享的文件:Android入门布局及UI相关案例
链接: https://pan.baidu.com/s/1EOuDUKJndMISolieFSvXXg?pwd=4k9n 提取码: 4k9n
引言
在 Android 应用中,RecyclerView 是最常用的列表展示组件,无论是商品列表、新闻流、视频推荐页,还是内容首页,几乎无处不在。初学者常常从单一类型列表入门,比如简单的文字列表或图文卡片;但一旦进入实际项目,我们面临的却是更加复杂的列表结构:
- 同一个页面中可能出现多种布局样式:顶部 Banner、大标题、横向滑动卡片、竖向新闻流……
- 用户操作或接口更新后,列表数据需要实时刷新,并尽可能做到精准刷新、避免性能浪费
也就是说,多类型布局支持 + 高性能刷新机制,几乎是中高级 RecyclerView 使用中绕不开的两个课题。
本篇我们将通过一个实战 Demo,从零开始实现这样一个典型的首页列表:
1. 页面顶部展示一张 Banner 图片
2. 中间是「推荐直播间」,每行显示两个直播卡片
3. 接着是「最新资讯」,展示若干新闻摘要
4. 支持两种刷新方式:
- 「全量刷新」:使用 notifyDataSetChanged()
- 「智能刷新」:使用 DiffUtil 精准对比变化项
接下来,我们先来看如何实现多类型布局的支持。
一、支持多类型 Item 的 RecyclerView
1.1 定义多类型数据模型
在实际业务中,RecyclerView 常常需要展示不止一种布局。例如电商首页中常见的结构就包括:
- 顶部的 Banner 区域;
- 内容分区标题,如“猜你喜欢”、“热门直播”;
- 模块内容卡片,如直播间、新闻资讯等。
因此我们要先将这些内容类型抽象成数据模型类,并配合布局实现可复用的多类型渲染。
✅ 本 Demo 中的四种模型:
类型 | 用途 | 数据类定义 |
---|---|---|
Banner | 顶部大图 | data class Banner(val imageResId: Int) |
TitleItem | 分组标题(如推荐直播) | data class TitleItem(val text: String) |
LiveItem | 直播卡片 | data class LiveItem(val title: String, val coverResId: Int) |
NewsItem | 新闻摘要卡片 | data class NewsItem(val title: String, val summary: String) |
这四个类分别代表页面中四种功能块,我们会将它们依次插入到列表数据源中(统一为 List<Any>),并在 Adapter 中通过类型判断进行区分渲染。
Banner:
data class Banner(val imageResId: Int
)
TitleItem:
data class TitleItem(val text: String
)
LiveItem:
data class LiveItem(val title: String,val coverResId: Int
)
NewsItem:
data class NewsItem(val title: String,val summary: String
)
1.2 Adapter 与 ViewHolder 实现
有了多类型的数据模型之后,下一步就是在 Adapter 中进行“识别”和“绑定”。在 RecyclerView 中,支持多类型的关键机制有两个:
✅ getItemViewType(position: Int): Int
这个方法用来告诉 RecyclerView:当前 position 对应的数据项属于哪种类型,我们可以为每个类型分配一个整数常量:
override fun getItemViewType(position: Int): Int {return when (items[position]) {is Banner -> TYPE_BANNERis TitleItem -> TYPE_TITLEis LiveItem -> TYPE_LIVEis NewsItem -> TYPE_NEWSelse -> throw IllegalArgumentException("未知类型")}
}
这样 RecyclerView 才知道该使用哪个布局去创建 ViewHolder。
✅ onCreateViewHolder(parent, viewType)
根据 viewType 创建不同类型的 ViewHolder 和对应布局:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {val inflater = LayoutInflater.from(parent.context)return when (viewType) {TYPE_BANNER -> BannerViewHolder(inflater.inflate(R.layout.item_banner, parent, false))TYPE_TITLE -> TitleViewHolder(inflater.inflate(R.layout.item_title, parent, false))TYPE_LIVE -> LiveViewHolder(inflater.inflate(R.layout.item_live, parent, false))TYPE_NEWS -> NewsViewHolder(inflater.inflate(R.layout.item_news, parent, false))else -> throw IllegalArgumentException("未知 viewType")}
}
✅ onBindViewHolder(holder, position)
再根据 position 取得数据并绑定到对应的 ViewHolder 上:
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {when (val item = items[position]) {is Banner -> (holder as BannerViewHolder).bind(item)is TitleItem -> (holder as TitleViewHolder).bind(item)is LiveItem -> (holder as LiveViewHolder).bind(item)is NewsItem -> (holder as NewsViewHolder).bind(item)}
}
✅ ViewHolder 示例
每个类型的 ViewHolder 都可定义为内部类,绑定对应控件:
class NewsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {private val title: TextView = itemView.findViewById(R.id.newsTitle)private val summary: TextView = itemView.findViewById(R.id.newsSummary)fun bind(item: NewsItem) {title.text = item.titlesummary.text = item.summary}
}
其他 ViewHolder 如 BannerViewHolder、LiveViewHolder 也类似,全部代码可以查看博客顶部的demo。
1.3 布局设计与 GridLayoutManager 使用
在基础用法中,我们通常使用 LinearLayoutManager 来实现 RecyclerView 的竖向排列。但当页面中存在需要「多列展示」的内容(如直播卡片),我们就可以借助 GridLayoutManager 实现灵活的布局排布。
✅ 目标排布效果:
Item 类型 | 排布方式 |
---|---|
Banner | 占整行(1 列 * 100%) |
TitleItem | 占整行 |
LiveItem | 每行显示两个(2 列) |
NewsItem | 占整行 |
✅ 使用 GridLayoutManager
在 MainActivity.kt 中设置 RecyclerView 的布局方式:
val layoutManager = GridLayoutManager(this, 2)
这里的 2 表示列表每行最多两列。
✅ 控制每种 item 的跨列数:SpanSizeLookup
我们不希望所有 item 都是两列的,而是只有 LiveItem 两列,其他类型都应该「占满整行」。为此我们通过 SpanSizeLookup 来动态指定每个 item 占几列:
layoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {override fun getSpanSize(position: Int): Int {return when (adapter.getItemViewType(position)) {HomeAdapter.TYPE_LIVE -> 1 // 每行两个else -> 2 // 占整行}}
}
这样就实现了「混合布局」的效果:LiveItem 为两列,其它都是一列跨两格。
✅ item 布局
item_banner.xml:
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"android:id="@+id/bannerImage"android:layout_width="match_parent"android:layout_height="180dp"android:scaleType="centerCrop"android:src="@drawable/placeholder"android:contentDescription="Banner" />
item_title.xml:
<TextView xmlns:android="http://schemas.android.com/apk/res/android"android:id="@+id/titleText"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="推荐直播"android:textSize="18sp"android:textStyle="bold"android:padding="16dp"android:textColor="#222222" />
item_live.xml:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical"android:padding="12dp"android:layout_width="match_parent"android:layout_height="wrap_content"><ImageViewandroid:id="@+id/liveImage"android:layout_width="match_parent"android:layout_height="160dp"android:scaleType="centerCrop"android:src="@drawable/placeholder" /><TextViewandroid:id="@+id/liveTitle"android:layout_width="match_parent"android:layout_height="wrap_content"android:paddingTop="8dp"android:text="直播标题"android:textSize="16sp"android:textColor="#333333" />
</LinearLayout>
item_news.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical"android:padding="12dp"android:layout_width="match_parent"android:layout_height="wrap_content"><TextViewandroid:id="@+id/newsTitle"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="资讯标题"android:textSize="16sp"android:textColor="#222222"android:textStyle="bold" /><TextViewandroid:id="@+id/newsSummary"android:layout_width="match_parent"android:layout_height="wrap_content"android:paddingTop="4dp"android:text="这是一个资讯概要内容..."android:textSize="14sp"android:textColor="#666666" />
</LinearLayout>
最终效果如下:
二、数据刷新机制的两种实现方式
RecyclerView 是一个高性能的列表控件,但要想实现真正“流畅”的体验,光靠展示还不够 —— 列表的数据经常会更新(新增、修改、删除),这时候就需要通过刷新机制来同步 UI。
在本 Demo 中,我们实现了两种刷新方式:
- ✅ 全量刷新(notifyDataSetChanged())
- ✅ 智能刷新(DiffUtil)
2.1 全量刷新(notifyDataSetChanged)
这是最常见、也是最基础的刷新方法。当你调用:
adapter.notifyDataSetChanged()
RecyclerView 会强制重绘整个列表,无论数据变化了多少项。虽然简单,但这带来了两个明显的问题:
- ❌ 性能低:所有可见项都要重新绑定;
- ❌ 无动画:看不到“哪一项”发生了变化,用户感知不明显。
实现如下:
findViewById<Button>(R.id.buttonFullRefresh).setOnClickListener {val newList = originalItems.toMutableList()// 修改两条 NewsItem 内容newList.replaceAll {if (it is NewsItem && it.title.contains("Kotlin")) {it.copy(summary = "Kotlin 2.0 已正式上线,快来试试吧!")} else if (it is NewsItem && it.title.contains("Compose")) {it.copy(summary = "Compose 新特性:自动响应式刷新")} else it}// 添加两条新的 LiveItem,插入在第一个 LiveItem 后val insertIndex = newList.indexOfFirst { it is LiveItem }if (insertIndex != -1) {newList.add(insertIndex, LiveItem("直播新品推荐", R.drawable.placeholder))newList.add(insertIndex + 1, LiveItem("夜间慢直播", R.drawable.placeholder))}// 替换原数据并全量刷新originalItems.clear()originalItems.addAll(newList)adapter.notifyDataSetChanged()}
✅ 使用场景:
- 快速原型开发;
- 页面结构很小、数据量不大;
- 数据变化剧烈、难以追踪变化项(如每次全替换)。
2.2 智能刷新(DiffUtil)
DiffUtil 是 Android 官方推荐的列表刷新工具,可以根据「旧数据」与「新数据」之间的差异,计算出:
- 哪些项需要插入、删除、更新;
- 哪些项保持不变、可复用。
✅ 核心用法:
val diffResult = DiffUtil.calculateDiff(diffCallback)
diffResult.dispatchUpdatesTo(adapter)
你需要实现一个 DiffUtil.Callback,并重写以下方法:
override fun areItemsTheSame(oldPos: Int, newPos: Int): Boolean {// 判断是否为同一条数据
}override fun areContentsTheSame(oldPos: Int, newPos: Int): Boolean {// 判断内容是否有变更
}
实现如下:
findViewById<Button>(R.id.buttonSmartRefresh).setOnClickListener {val newList = originalItems.toMutableList()// 修改两条 NewsItem 内容newList.replaceAll {if (it is NewsItem && it.title.contains("Kotlin")) {it.copy(summary = "Kotlin 2.0 已正式上线,快来试试吧!")} else if (it is NewsItem && it.title.contains("Compose")) {it.copy(summary = "Compose 新特性:自动响应式刷新")} else it}// 添加两条新的 LiveItem,插入在第一个 LiveItem 后val insertIndex = newList.indexOfFirst { it is LiveItem }if (insertIndex != -1) {newList.add(insertIndex, LiveItem("直播新品推荐", R.drawable.placeholder))newList.add(insertIndex + 1, LiveItem("夜间慢直播", R.drawable.placeholder))}val diffCallback = object : DiffUtil.Callback() {override fun getOldListSize() = originalItems.sizeoverride fun getNewListSize() = newList.sizeoverride fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {return originalItems[oldItemPosition] == newList[newItemPosition]}override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {return originalItems[oldItemPosition] == newList[newItemPosition]}}val diffResult = DiffUtil.calculateDiff(diffCallback)originalItems.clear()originalItems.addAll(newList)diffResult.dispatchUpdatesTo(adapter)}
✅ 使用场景:
- 大量数据频繁变化;
- 想要提升滚动流畅度;
- 希望有插入/删除动画过渡。
结语
在本篇实战中,我们围绕一个首页式的列表页面,完整实现了 RecyclerView 在复杂场景下的两大关键能力:
✅ 多类型布局支持
- 通过定义多个数据模型(Banner、TitleItem、LiveItem、NewsItem),实现页面结构化组织;
- 使用 getItemViewType() 方法判断类型,搭配多个布局文件实现灵活展示;
- 借助 GridLayoutManager + SpanSizeLookup 实现不同 item 的排布策略,兼顾视觉和性能。
✅ 数据刷新机制
- 使用 notifyDataSetChanged() 实现最基础的全量刷新;
- 使用 DiffUtil 实现性能更优、体验更佳的智能刷新;
- 对比展示了两种刷新机制在实现与效果上的差异,方便在项目中做出合理选择。
📈 可扩展方向
如果你已经掌握了本文的内容,下面这些方向将进一步提升你的列表开发能力:
扩展点 | 描述 |
---|---|
点击事件封装 | 为不同类型的 item 添加点击、长按等事件回调 |
加载更多 / 分页 | 实现滑动到底部自动加载下一页内容 |
空数据 / 错误状态处理 | 在数据为空或加载失败时展示占位 UI |
多类型封装优化 | 用委托或泛型方式封装多类型 Adapter,简化维护 |
使用 Paging3 | 对接分页接口,自动处理刷新与数据合并 |
Jetpack Compose 实现 | 通过 Compose 编写声明式列表,更加现代化 |