当前位置: 首页 > news >正文

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 编写声明式列表,更加现代化

http://www.xdnf.cn/news/1244323.html

相关文章:

  • AI 对话高效输入指令攻略(四):AI+Apache ECharts:生成各种专业图表
  • 【学习笔记】Manipulate-Anything(基于视觉-语言模型的机器人自动化操控系统)
  • 【09】C++实战篇——C++ 生成静态库.lib 及 C++调用lib,及实际项目中的使用技巧
  • javacc学习笔记 02、JavaCC 语法描述文件的格式解析
  • Druid手写核心实现案例 实现一个简单Select 解析,包含Lexer、Parser、AstNode
  • k8s常见问题
  • (论文速读)RMT:Retentive+ViT的视觉新骨干
  • 20250805问答课题-实现TextRank + 问题分类
  • 力扣热题100------21.合并两个有序链表
  • 8.高斯混合模型
  • k8s简介
  • 数据集相关类代码回顾理解 | np.mean\transforms.Normalize\transforms.Compose\xxx.transform
  • Claude Code六周回顾
  • 补:《每日AI-人工智能-编程日报》--2025年7月29日
  • steam Rust游戏 启动错误,删除sys驱动,亲测有效。
  • 机器学习(13):逻辑回归
  • 昇思学习营-模型推理和性能优化学习心得
  • ShowDoc与Docmost对比分析:开源文档管理工具的选择指南
  • 【QT】常⽤控件详解(四)常用显示类控件类 Label LCDNumber ProgressBar Calendar Widget
  • [Oracle] TO_NUMBER()函数
  • HTTPS有哪些优点
  • 【OS】操作系统概述
  • 蓝桥杯----AT24C02
  • 机器学习(12):拉索回归Lasso
  • Docker-07.Docker基础-数据卷挂载
  • 基于SpringBoot的OA办公系统的设计与实现
  • 小鹏汽车前端面经
  • 深度解析:CPU 与 GPU 上的张量运算,为何“快”与“慢”并非绝对?
  • Flutter 对 Windows 不同版本的支持及 flutter_tts 兼容性指南
  • C语言:构造类型学习