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.pswp.cn/bicheng/92241.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/92241.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何学习跨模态对齐(尤其是 CLIP 思想)

学习跨模态对齐&#xff08;尤其是CLIP思想&#xff09;需要结合理论基础、经典模型原理、实践复现和前沿扩展&#xff0c;以下是一套系统的学习路径&#xff0c;从入门到深入逐步展开&#xff1a; 一、先补基础&#xff1a;跨模态对齐的“前置知识” 跨模态对齐的核心是让图…

日记研究:一种深入了解用户真实体验的UX研究方法

在用户体验&#xff08;UX&#xff09;研究中&#xff0c;我们常常需要了解用户在真实世界中如何与产品互动。然而&#xff0c;由于时间和空间的限制&#xff0c;我们很难像“特工”一样全天候跟踪用户。这时&#xff0c;“日记研究”&#xff08;Diary Studies&#xff09;就成…

鸿蒙app 开发中 加载图片的时候闪一下 如何解决

1.解决 在图片上 加载这个属性 .syncLoad(true) 参考的官方链接

【OS】进程与线程

进程进程实体代码段相关数据PCB进程标识符外部标识符&#xff1a;为方便用户对进程的访问&#xff0c;为每个进程设置一个外部标识符&#xff0c;通常由字母和数字组成内部标识符&#xff1a;为方便系统对进程的使用&#xff0c;在OS中又为进程设置了内部标识符&#xff0c;赋予…

Django 序列化详解:从 Model 到 JSON,全面掌握数据转换机制

一、引言&#xff1a;什么是 Django 序列化&#xff1f;在 Web 开发中&#xff0c;序列化&#xff08;Serialization&#xff09; 是指将复杂的数据结构&#xff08;如数据库模型对象&#xff09;转换为可传输的格式&#xff08;如 JSON、XML、YAML 等&#xff09;&#xff0c;…

茶叶蛋大冒险小游戏流量主微信抖音小程序开源

游戏特点 响应式设计&#xff1a;完美适配各种移动设备屏幕尺寸 直观的触摸控制&#xff1a;左右滑动屏幕控制茶叶蛋移动 中式风格元素&#xff1a; 茶叶蛋角色带有裂纹纹理和可爱表情 筷子、蒸笼等中式厨房元素作为障碍物 八角、茶叶等香料作为收集物 锅底火焰动画效果 游戏机…

区分邮科工业交换机与路由器

在这个数字化的时代&#xff0c;我们每天都在享受着互联网带来的便利。无论是工作还是娱乐&#xff0c;网络已经成为我们生活中不可或缺的一部分。然而&#xff0c;在这个看似简单的背后&#xff0c;隐藏着两个至关重要的设备——邮科工业交换机和路由器。它们就像网络世界的双…

【数据结构入门】数组和链表的OJ题(2)

目录 1.回文链表 分析&#xff1a; 代码&#xff1a; 2.相交链表 分析&#xff1a; 代码&#xff1a; 3.环形链表 分析&#xff1a; 代码&#xff1a; 面试提问&#xff1a; 4.环形链表II 分析1&#xff1a; 分析2&#xff1a; 代码&#xff1a; 5.随机链表的复…

文件包含篇

web78 第一题filter伪协议直接读源码即可 ?filephp://filter/convert.base64-encode/resourceflag.php web79 flag.php的php无法用大小写绕过&#xff0c;所以用Php://input只读流 import requests url "http://fadb524a-f22d-4747-a35c-82f71e84bba7.challenge.ctf.sho…

互作蛋白组学技术对比:邻近标记与传统IP-MS、Pull down-MS优势对比

在生命科学领域&#xff0c;蛋白质间的相互作用构成了生命活动的核心网络&#xff0c;驱动着信号传导、基因调控、代谢途径等关键过程。为了绘制这幅复杂的“分子互作地图”&#xff0c;科学家们开发了多种技术&#xff0c;其中免疫共沉淀结合质谱&#xff08;IP-MS&#xff09…

(ZipList入门笔记一)ZipList的节点介绍

ZipList是 Redis 中一种非常紧凑、节省内存的数据结构 Ziplist&#xff08;压缩列表&#xff09; 的内部内存布局。它被用于存储元素较少的 List、Hash 和 Zset。 下面我们来详细介绍每一个节点的含义&#xff1a; 1. zlbytes (ziplist bytes) 含义&#xff1a; 整个压缩列…

Unix 发展史概览

这里是一个简明清晰的 Unix 发展史概览&#xff0c;涵盖从起源到现代的重要节点和演变过程。Unix 发展史概览 1. Unix 起源&#xff08;1969年&#xff09; 贝尔实验室&#xff1a;Ken Thompson 和 Dennis Ritchie 开发出 Unix 操作系统。最初设计目标&#xff1a;简洁、可移植…

基于coze studio开源框架二次定制开发教程

目录 一、 项目介绍 1.1 什么是Coze Studio 1.2 功能清单 1.3对比商业版本 二、 功能定开说明 2.1 技术栈简介 2.2 项目架构

RHCE认证题解

考前说明请勿更改 IP 地址。DNS 解析完整主机名&#xff0c;同时也解析短名称。• 所有系统的 root 密码都是 redhat• Ansible 控制节点上已创建用户账户 devops。可以使用 ssh 访问• 所需的所有镜像保存在镜像仓库 utility.lab.example.compodman 可使用下述账号登录使用 用…

调用com对象的坑

1、谏言 最近我在弄64位调用32位dll的问题&#xff0c;在几种IPC之间&#xff0c;最后考虑了调用COM 毕竟我们只在windows平台 2、第一坑–修改编译后都需要重新注册&#xff0c;注册表 一直以为只需要编译就好了&#xff0c;结果调用没反应、报错什么的&#xff0c;需要先撤销…

【Python】PyQt 实现 TreeWidget 多级联动选择逻辑,打造素材搜索自定义树形控件!

在开发自己的写作素材管理工具时,我遇到了一个非常典型但又略显棘手的 UI 问题: 💡 如何实现一个“可自由勾选分类标签”的树形结构界面,支持父子节点自动联动勾选,提升用户体验? 虽然 PyQt 的 QTreeWidget 是构建多层分类结构的好帮手,但默认却不具备父子节点的自动级…

27-数据仓库与Apache Hive-2

1.数仓开发语言概述 理论上来说&#xff0c;任何一款编程语言只要具备读写数据、处理数据的能力&#xff0c;都可以用于数仓的开发。比如大家耳熟能详的C、java、Python等&#xff1b; 关键在于编程语言是否易学、好用、功能是否强大。遗憾的是上面所列出的C、Python等编程语言…

软件测试——接口自动化

测试中的自动化分为两类&#xff1a; 1.ui自动化&#xff08;web、移动端&#xff09;2.接口自动化 前面的博客中&#xff0c;我们已经讲解了web端的ui自动化&#xff0c;感兴趣的同学可以去看看&#xff1a;软件测试——自动化测试常见函数_自动化测试代码编写-CSDN博客 今…

Flask一个用户同时只能在一处登录实现

场景&#xff1a;web页面如果多人用同一账号同时登录操作&#xff0c;可能会导致数据等的混乱甚至出现故障。并且可能损害开发者的利益。为此&#xff0c;本篇文章就讲下如何实现同一账户同时仅能一个地方登录操作。 思路&#xff1a;1. 用户登陆时生成token&#xff08;uuid.u…

联发科芯片组曝高危漏洞:越界写入缺陷危及智能手机与物联网设备安全

漏洞概况全球领先的芯片组制造商联发科&#xff08;MediaTek&#xff09;近日发布最新产品安全公告&#xff0c;披露了影响其智能手机、物联网设备及其他嵌入式系统芯片的多项安全漏洞。高危漏洞分析CVE-2025-20696 作为公告披露的首个且最严重的漏洞&#xff0c;该高危缺陷源于…