鸿蒙 ArkUI 列表底部加载更多指示器实现详解


鸿蒙 ArkUI 列表底部加载更多指示器实现详解
——从 Flutter ListView.builder 到鸿蒙 List 组件的完整迁移实践
一、引言
在移动端应用开发中,列表(List)是最核心、最高频的 UI 组件之一。无论是社交应用的信息流、电商平台的商品列表,还是新闻客户端的文章列表,都离不开对大量数据的展示。当数据量较大时,一次性加载全部数据会带来两个严重问题:首屏加载时间过长 和 内存占用过高。因此,分页加载(Pagination)成为移动端列表的标配能力。
在分页加载的交互设计中,底部加载更多指示器(Load More Indicator)是最常见且用户感知最直观的方案之一。当用户滚动到列表底部时,显示一个包含旋转进度圈和提示文字的加载指示器,告诉用户"正在加载更多数据",待数据加载完成后自动将新数据追加到列表末尾,并移除加载指示器(或隐藏它,等待下一次触发)。
本文将以一个实际的鸿蒙(HarmonyOS)ArkUI 项目为例,详细讲解如何在 List 组件中实现底部加载更多指示器,并与 Flutter 的 ListView.builder 方案进行对比分析,帮助开发者理解两种框架在列表分页加载上的设计异同。文章将涵盖状态管理、滚动监听、异步加载模拟、搜索与分页联动等关键主题,全文约 10000 字,适合有一定鸿蒙或 Flutter 基础的开发者阅读。
二、背景知识
2.1 什么是分页加载
分页加载是一种数据获取策略,它将一个大数据集切分成多个小的"页"(Page),每次只加载一页数据。用户通过某种交互(点击按钮、滚动到底部、上拉刷新等)触发下一页数据的加载。分页加载的核心优势包括:
- 降低首屏加载时间:只加载用户当前可见的数据,减少网络请求和数据解析的时间
- 减少内存占用:列表项按需渲染,不一次性创建大量组件实例
- 节省流量:用户可能根本不会浏览全部数据,按需加载避免了不必要的网络传输
- 提升用户体验:应用响应更快,交互更流畅
分页加载通常有两种交互模式:
- 显式分页:在列表底部显示"加载更多"按钮,用户点击后加载下一页。这种方式给予用户完全的控制权,但增加了交互步骤。
- 隐式分页(无限滚动):当用户滚动到列表底部时自动触发加载。这种方式交互更自然,但可能让用户感到失控(突然加载到不想看的内容)。
2.2 底部加载更多指示器的设计目标
一个优秀的底部加载更多指示器应当满足以下设计目标:
- 可见性:用户滚动到列表底部时,能清晰地看到加载指示器,知道数据正在加载中
- 反馈性:加载过程中要有明确的视觉反馈(旋转动画、进度条等),告知用户系统正在工作
- 状态明确:区分"加载中"、“加载完成”、"已无更多数据"三种状态,每种状态都有对应的 UI 表现
- 非侵入性:加载指示器不应遮挡已有内容,不应影响用户浏览已加载的数据
- 性能友好:指示器本身的渲染开销应尽可能小,不影响列表的滚动性能
2.3 Flutter ListView.builder 的标准做法
在 Flutter 中,实现列表底部加载更多指示器的标准做法是利用 ListView.builder 的 itemBuilder 回调,在最后一项返回一个包含加载指示器的 widget。典型代码如下:
ListView.builder(
itemCount: items.length + (hasMore ? 1 : 0),
itemBuilder: (context, index) {
if (index == items.length) {
// 最后一项:加载更多指示器
return Column(
children: [
CircularProgressIndicator(),
SizedBox(height: 8),
Text('加载更多...'),
],
);
}
// 正常列表项
return ListTile(title: Text(items[index]));
},
controller: _scrollController,
)
Flutter 方案的核心思路是:将加载指示器视为列表中的一个特殊 item,通过 itemCount 的巧妙控制来决定是否显示它。当 hasMore = true 时,itemCount 比实际数据多 1,itemBuilder 在最后一个索引处返回加载指示器;当所有数据加载完毕,hasMore = false,itemCount 等于数据长度,加载指示器自然消失。
这种设计简洁优雅,将"加载指示器"作为列表的一个普通节点来对待,统一了渲染逻辑。
三、鸿蒙 ArkUI 中的对等实现
3.1 框架对比:Flutter ListView.builder vs 鸿蒙 List
在开始编码之前,先对比两类框架在列表组件上的核心差异:
| 特性 | Flutter | 鸿蒙 ArkUI |
|---|---|---|
| 列表组件 | ListView.builder |
List |
| 子项构建 | itemBuilder 回调 |
ForEach / LazyForEach |
| 滚动监听 | ScrollController + addListener |
onScrollIndex 事件 |
| 进度指示器 | CircularProgressIndicator |
LoadingProgress |
| 状态管理 | StatefulWidget + setState |
@State 装饰器 |
| 条件渲染 | 三元表达式 / if 语句 |
if 条件语句 |
虽然语法不同,但核心思想高度一致:
- 使用
@State(对应 Flutter 的setState)驱动 UI 更新 - 将加载指示器作为列表的一个特殊子项
- 通过滚动事件监听触发加载逻辑
- 用
if条件控制加载指示器的显示/隐藏
3.2 项目结构概览
本文示例项目是一个鸿蒙 ArkUI 应用,目录结构如下:
a12/
├── entry/src/main/ets/
│ ├── entryability/EntryAbility.ets # Ability 入口
│ └── pages/Index.ets # 主页面(列表 + 搜索)
├── AppScope/ # 应用级配置
├── hvigor/ # 构建配置
└── build-profile.json5 # 构建配置文件
核心代码集中在 Index.ets 文件中,该文件包含一个带有搜索过滤功能的列表组件,并实现了分页加载和底部加载更多指示器。
3.3 核心状态变量设计
鸿蒙 ArkUI 使用 @State 装饰器标记响应式状态变量。当状态变化时,框架自动重新渲染依赖该状态的 UI 部分。以下是本例中使用的核心状态变量:
// 搜索关键词
@State searchKeyword: string = ''
// 全量数据源(模拟后端数据库)
@State fullDataSource: Array<string> = [
'Apple iPhone 15 Pro Max',
'Huawei Mate 60 Pro',
// ... 共 20 条数据
]
// 当前分页展示的数据(经过搜索过滤 + 分页截取)
@State filteredDataSource: Array<string> = []
// 当前页码(从 1 开始)
@State currentPage: number = 1
// 是否正在加载更多数据(防止重复请求)
@State isLoadingMore: boolean = false
// 是否还有更多数据可供加载(控制底部指示器显示)
@State hasMoreData: boolean = true
// 每页数据条数
private pageSize: number = 5
变量设计说明:
filteredDataSource是实际展示在列表中的数据,它同时受搜索关键词和分页影响currentPage记录当前已加载到第几页,每次加载更多时自增isLoadingMore是一个锁标志,防止在加载过程中重复触发加载请求hasMoreData控制底部指示器的显示与隐藏,当所有数据加载完毕时设为falsepageSize使用private而非@State,因为它是常量,不需要响应式追踪
3.4 数据初始化与加载逻辑
在组件挂载后,aboutToAppear 生命周期回调中调用 loadPageData() 加载第一页数据:
aboutToAppear() {
this.loadPageData()
}
loadPageData() 方法的核心逻辑:
loadPageData(): void {
let keyword = this.searchKeyword.toLowerCase()
// 1. 根据搜索关键词过滤数据源
let source: Array<string> = keyword.length > 0
? this.fullDataSource.filter(item => item.toLowerCase().includes(keyword))
: this.fullDataSource.slice()
// 2. 根据当前页码计算截取终点
let endIndex = this.pageSize * this.currentPage
if (endIndex >= source.length) {
endIndex = source.length
this.hasMoreData = false
} else {
this.hasMoreData = true
}
// 3. 截取前 N 条数据作为展示列表
this.filteredDataSource = source.slice(0, endIndex)
}
这个方法的设计要点:
- 搜索与分页的解耦:先按关键词过滤,再按页码截取,两个逻辑互不干扰
- 边界处理:当
endIndex超过数据源长度时,回退到数据源长度,并标记hasMoreData = false - 响应式驱动:修改
filteredDataSource和hasMoreData后,框架自动重绘 UI
3.5 加载更多方法
loadMoreData() 方法模拟异步加载下一页数据:
loadMoreData(): void {
// 防止重复请求
if (this.isLoadingMore || !this.hasMoreData) {
return
}
this.isLoadingMore = true
// 模拟 1.5 秒网络延迟
setTimeout(() => {
this.currentPage++
let keyword = this.searchKeyword.toLowerCase()
let source: Array<string> = keyword.length > 0
? this.fullDataSource.filter(item => item.toLowerCase().includes(keyword))
: this.fullDataSource.slice()
let endIndex = this.pageSize * this.currentPage
if (endIndex >= source.length) {
endIndex = source.length
this.hasMoreData = false
} else {
this.hasMoreData = true
}
this.filteredDataSource = source.slice(0, endIndex)
this.isLoadingMore = false
}, 1500)
}
关键设计决策:
- 双重保护:
isLoadingMore和hasMoreData双重检查,确保不会在无数据可加载时触发请求,也不会在加载过程中触发重复请求 - 模拟延迟:使用
setTimeout模拟 1.5 秒的网络延迟,让加载指示器有足够的时间展示给用户 - 页码自增:成功加载后
currentPage++,为下一次加载做准备 - 解锁:加载完成后
isLoadingMore = false,允许下一次触发
3.6 列表与底部指示器的 UI 构建
这是整个实现中最核心的 UI 部分——在 List 组件中,将加载指示器作为一个特殊的 ListItem 追加在 ForEach 之后:
List({ space: 8 }) {
// 1. 正常数据项
ForEach(this.filteredDataSource, (item: string) => {
ListItem() {
Row() {
Image($r('app.media.startIcon'))
.width(40).height(40).borderRadius(8)
.objectFit(ImageFit.Cover)
Text(item)
.fontSize(16).fontColor('#333333')
.fontWeight(FontWeight.Medium)
.margin({ left: 12 })
.layoutWeight(1)
}
.width('100%')
.padding({ left: 16, right: 16, top: 14, bottom: 14 })
.backgroundColor('#FFFFFF')
.borderRadius(12)
}
.width('100%')
})
// 2. 底部加载更多指示器(条件渲染)
if (this.hasMoreData) {
ListItem() {
Column() {
LoadingProgress()
.width(32).height(32).color('#007DFF')
Text('加载更多...')
.fontSize(14).fontColor('#999999')
.margin({ top: 8 })
}
.width('100%')
.height(80)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
.width('100%')
}
}
设计要点解析:
ForEach+ 条件ListItem的混合构建:这是鸿蒙 ArkUI 中实现"异质列表"(heterogeneous list)的标准模式。ForEach负责渲染同质的数据项,单独的if条件块负责渲染特殊项(加载指示器)。- 条件式隐藏:
if (this.hasMoreData)确保当所有数据加载完毕时,底部指示器自动从组件树中移除,不占用任何渲染资源。 LoadingProgress组件:鸿蒙 ArkUI 的LoadingProgress对应 Flutter 的CircularProgressIndicator,是一个旋转的圆形进度指示器,默认带有动画效果。- 布局居中:
Column使用justifyContent(FlexAlign.Center)和alignItems(HorizontalAlign.Center)实现内容在垂直和水平方向上的居中。 - 固定高度:
height(80)为加载指示器预留了足够的垂直空间,防止视觉上"贴边"。
3.7 滚动监听触发加载
这是整个流程的"触发器"——通过 List 的 onScrollIndex 事件监听滚动位置,当滚动到接近列表底部时自动调用 loadMoreData():
List({ space: 8 }) {
// ... 列表内容
}
.width('100%')
.layoutWeight(1)
.padding({ left: 16, right: 16, top: 16 })
.onScrollIndex((firstIndex: number, lastIndex: number) => {
// 当滚动到倒数第 2 项时,触发加载更多
if (lastIndex >= this.filteredDataSource.length - 2
&& !this.isLoadingMore && this.hasMoreData) {
this.loadMoreData()
}
})
触发阈值的考量:
- 使用
lastIndex >= this.filteredDataSource.length - 2而非lastIndex === this.filteredDataSource.length - 1,是为了在用户滚动到倒数第 2 项时就提前触发加载。这样当用户真正滚动到底部时,数据可能已经加载完毕,实现了"无感加载"的体验。 - 如果数据加载速度较慢(网络延迟高),这个提前量可以给用户更流畅的体验;如果加载速度很快,用户甚至不会注意到列表底部曾经出现过加载指示器。
三重守卫条件:
lastIndex >= this.filteredDataSource.length - 2:滚动位置接近底部!this.isLoadingMore:当前没有正在进行的加载请求this.hasMoreData:后端还有更多数据可供加载
三个条件同时满足才会触发加载,最大限度地避免了不必要的请求。
3.8 搜索与分页的联动
当用户输入搜索关键词时,需要重置分页状态并重新加载:
TextInput({ placeholder: $r('app.string.search_placeholder') })
// ... 样式属性
.onChange((value: string) => {
this.searchKeyword = value
this.currentPage = 1 // 重置页码
this.hasMoreData = true // 重置"更多数据"标志
this.isLoadingMore = false // 重置加载锁
this.loadPageData() // 重新加载第一页
})
为什么需要重置三个状态?
currentPage = 1:搜索关键词变化后,数据范围完全不同,需要从第一页开始hasMoreData = true:新搜索可能返回大量结果,需要重新检查是否有更多数据isLoadingMore = false:解除加载锁,允许新的加载触发
四、完整代码与逐行解读
为便于理解,以下给出完整的 Index.ets 代码,并附上结构化注释:
@Entry
@Component
struct SearchFilterList {
// ========== 状态变量 ==========
@State searchKeyword: string = ''
@State fullDataSource: Array<string> = [
'Apple iPhone 15 Pro Max', 'Huawei Mate 60 Pro',
'Samsung Galaxy S24 Ultra', 'Xiaomi 14 Pro',
'OPPO Find X7 Ultra', 'vivo X100 Pro',
'Honor Magic6 Pro', 'Realme GT5 Pro',
'OnePlus 12', 'Google Pixel 8 Pro',
'Sony Xperia 1 V', 'LG V60 ThinQ',
'Motorola Edge 40 Pro', 'Nokia G60',
'Asus ROG Phone 8', 'Black Shark 5 Pro',
'Lenovo Legion Phone', 'ZTE Axon 50 Ultra',
'Meizu 20 Pro', 'Tecno Phantom X2'
]
@State filteredDataSource: Array<string> = []
@State currentPage: number = 1
@State isLoadingMore: boolean = false
@State hasMoreData: boolean = true
private pageSize: number = 5
// ========== 生命周期 ==========
aboutToAppear() {
this.loadPageData()
}
// ========== 数据加载方法 ==========
/** 加载当前页数据(不包含异步延迟) */
loadPageData(): void {
let keyword = this.searchKeyword.toLowerCase()
let source: Array<string> = keyword.length > 0
? this.fullDataSource.filter(item => item.toLowerCase().includes(keyword))
: this.fullDataSource.slice()
let endIndex = this.pageSize * this.currentPage
if (endIndex >= source.length) {
endIndex = source.length
this.hasMoreData = false
} else {
this.hasMoreData = true
}
this.filteredDataSource = source.slice(0, endIndex)
}
/** 加载更多(模拟异步请求) */
loadMoreData(): void {
if (this.isLoadingMore || !this.hasMoreData) return
this.isLoadingMore = true
setTimeout(() => {
this.currentPage++
let keyword = this.searchKeyword.toLowerCase()
let source: Array<string> = keyword.length > 0
? this.fullDataSource.filter(item => item.toLowerCase().includes(keyword))
: this.fullDataSource.slice()
let endIndex = this.pageSize * this.currentPage
if (endIndex >= source.length) {
endIndex = source.length
this.hasMoreData = false
} else {
this.hasMoreData = true
}
this.filteredDataSource = source.slice(0, endIndex)
this.isLoadingMore = false
}, 1500)
}
// ========== UI 构建 ==========
build() {
Column() {
// 搜索输入框
TextInput({ placeholder: $r('app.string.search_placeholder') })
.width('100%').height(56).backgroundColor('#F5F5F5')
.borderRadius(28).padding({ left: 20, right: 20 })
.fontSize(16)
.onChange((value: string) => {
this.searchKeyword = value
this.currentPage = 1
this.hasMoreData = true
this.isLoadingMore = false
this.loadPageData()
})
// 列表
List({ space: 8 }) {
// 数据项
ForEach(this.filteredDataSource, (item: string) => {
ListItem() {
Row() {
Image($r('app.media.startIcon'))
.width(40).height(40).borderRadius(8)
.objectFit(ImageFit.Cover)
Text(item)
.fontSize(16).fontColor('#333333')
.fontWeight(FontWeight.Medium)
.margin({ left: 12 }).layoutWeight(1)
}
.width('100%')
.padding({ left: 16, right: 16, top: 14, bottom: 14 })
.backgroundColor('#FFFFFF').borderRadius(12)
}.width('100%')
})
// 底部加载更多指示器
if (this.hasMoreData) {
ListItem() {
Column() {
LoadingProgress().width(32).height(32).color('#007DFF')
Text('加载更多...')
.fontSize(14).fontColor('#999999')
.margin({ top: 8 })
}
.width('100%').height(80)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}.width('100%')
}
}
.width('100%').layoutWeight(1)
.padding({ left: 16, right: 16, top: 16 })
.onScrollIndex((firstIndex: number, lastIndex: number) => {
if (lastIndex >= this.filteredDataSource.length - 2
&& !this.isLoadingMore && this.hasMoreData) {
this.loadMoreData()
}
})
// 空结果提示
if (this.filteredDataSource.length === 0 && this.searchKeyword.length > 0) {
Column() {
Text($r('app.string.no_result'))
.fontSize(14).fontColor('#999999').margin({ bottom: 8 })
Text(this.searchKeyword)
.fontSize(14).fontColor('#007DFF')
.fontWeight(FontWeight.Medium)
}
.width('100%').height(200)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
}
.width('100%').height('100%')
.backgroundColor('#F0F2F5')
.padding({ top: 16, left: 16, right: 16 })
}
}
五、技术深度解析
5.1 鸿蒙 ArkUI 的响应式更新机制
鸿蒙 ArkUI 采用声明式 UI 编程范式,其核心是状态驱动 UI 更新。当 @State 装饰的变量发生变化时,框架会自动重新渲染依赖于该变量的 UI 部分。
在我们的实现中,数据流如下:
用户滚动列表
↓
onScrollIndex 事件触发
↓
调用 loadMoreData()
↓
isLoadingMore = true(UI 更新:显示加载指示器)
↓
setTimeout 异步回调
↓
currentPage++、filteredDataSource 追加新数据、
hasMoreData 更新、isLoadingMore = false
↓
UI 自动更新:列表新增数据、加载指示器隐藏/保留
这个流程中,开发者只需要修改状态变量,不需要手动操作 DOM 或组件树,框架负责增量更新。
5.2 与 Flutter 方案的核心差异
虽然整体思路相似,但鸿蒙 ArkUI 和 Flutter 在具体实现上有几个关键差异:
| 维度 | Flutter | 鸿蒙 ArkUI |
|---|---|---|
| 列表构建方式 | itemBuilder 回调函数,按需调用 |
ForEach 遍历数组,声明式构建 |
| 特殊项处理 | 通过 itemCount + 1 和 index 判断 |
在 ForEach 之后追加条件 ListItem |
| 状态更新 | setState() 显式触发重建 |
@State 变化自动驱动重建 |
| 滚动监听 | ScrollController + addListener |
onScrollIndex 事件回调 |
| 异步加载 | Future / async-await |
setTimeout / Promise |
Flutter 方式更灵活,可以精确控制每一个索引位置渲染什么内容,但需要手动管理 itemCount 的计算和 index 的逻辑分支。
鸿蒙 ArkUI 方式更直观,将列表视为"数据项集合 + 特殊项",通过 ForEach 遍历数据和 if 条件渲染特殊项,代码更接近自然语言,可读性更好。
5.3 性能优化考量
1. 条件渲染而非样式隐藏
本例中,当 hasMoreData = false 时,使用 if 条件将加载指示器完全从组件树中移除,而非通过 opacity 或 visibility 隐藏它。这样做的好处是:
- 减少组件树深度,降低布局计算开销
- 减少内存占用,释放已创建的组件实例
- 避免不必要的测量和绘制
2. 提前触发加载
使用 lastIndex >= this.filteredDataSource.length - 2 而非 lastIndex >= this.filteredDataSource.length - 1 作为触发条件,可以在用户到达列表底部之前就开始加载数据。这利用了用户的滚动时间窗口,使得数据加载和用户滚动可以并行进行,减少用户等待时间。
3. 加载锁防止重复请求
isLoadingMore 标志位确保在上一轮加载完成之前,不会触发新的加载请求。这避免了:
- 多次发起相同的网络请求
- 数据重复追加到列表
- 列表渲染闪烁
5.4 边界情况处理
情况一:数据量刚好是页大小的整数倍
当 fullDataSource.length 恰好等于 pageSize * N 时,最后一页加载完毕后 endIndex 等于 source.length,hasMoreData 设为 false,指示器正确隐藏。
情况二:搜索后无结果
当搜索关键词没有匹配任何数据时,filteredDataSource 为空数组,空结果提示(“无结果”)显示,列表无内容,自然不会触发加载更多。
情况三:搜索后数据不足一页
当搜索过滤后的数据量少于 pageSize 时,loadPageData() 会一次性加载全部数据,hasMoreData 设为 false,底部不显示加载指示器。
情况四:快速连续滚动
如果用户快速滚动到列表底部后又快速往上滚动再到底部,由于 isLoadingMore 锁的存在,同一时间只会有一个加载请求在执行,不会出现数据重复或顺序错乱。
六、进阶扩展
6.1 添加加载失败重试
实际项目中,网络请求可能失败。可以在加载指示器中添加重试机制:
@State loadError: boolean = false
loadMoreData(): void {
if (this.isLoadingMore || !this.hasMoreData) return
this.isLoadingMore = true
this.loadError = false
// 模拟网络请求
setTimeout(() => {
try {
// 正常加载逻辑...
} catch (e) {
this.loadError = true
this.isLoadingMore = false
}
}, 1500)
}
// UI 中根据状态显示不同内容
if (this.hasMoreData) {
ListItem() {
Column() {
if (this.loadError) {
Text('加载失败,点击重试')
.fontSize(14).fontColor('#FF4444')
.onClick(() => this.loadMoreData())
} else {
LoadingProgress().width(32).height(32).color('#007DFF')
Text('加载更多...').fontSize(14).fontColor('#999999')
}
}
// ...
}
}
6.2 显示"已加载全部"
当所有数据加载完毕时,可以显示一个友好的提示而非直接消失:
if (!this.hasMoreData && this.filteredDataSource.length > 0) {
ListItem() {
Text('— 已加载全部数据 —')
.fontSize(13).fontColor('#CCCCCC')
.textAlign(TextAlign.Center)
.width('100%').height(50)
}
}
6.3 使用 LazyForEach 进行性能优化
对于真正的超长列表(数千条数据),建议使用 LazyForEach 替代 ForEach。LazyForEach 只在列表项即将进入可视区域时才创建组件实例,离开可视区域时销毁,大幅降低内存占用:
List({ space: 8 }) {
LazyForEach(this.dataSource, (item: string) => {
ListItem() { /* ... */ }
}, (item: string) => item)
if (this.hasMoreData) {
ListItem() { /* 加载指示器 */ }
}
}
使用 LazyForEach 需要实现 IDataSource 接口,此处不再展开。
6.4 与下拉刷新的联动
在实际应用中,底部加载更多通常与顶部下拉刷新配合使用。鸿蒙 ArkUI 提供了 Refresh 组件:
Refresh() {
List({ space: 8 }) {
// ... 列表内容
}
}
.onRefresh(() => {
// 重置分页,重新加载
this.currentPage = 1
this.hasMoreData = true
this.loadPageData()
})
七、Flutter 与鸿蒙 ArkUI 实现对比总结
7.1 代码量对比
| 功能点 | Flutter (Dart) | 鸿蒙 ArkUI (TypeScript) |
|---|---|---|
| 状态定义 | ~15 行 | ~8 行 |
| 数据加载 | ~20 行 | ~25 行 |
| 列表构建 | ~10 行 | ~15 行 |
| 滚动监听 | ~8 行 | ~5 行 |
| 搜索联动 | ~5 行 | ~10 行 |
| 总计 | ~58 行 | ~63 行 |
两者在代码量上基本相当,鸿蒙 ArkUI 略多几行主要是由于 @State 装饰器的显式声明和 TypeScript 的类型注解。
7.2 可读性对比
Flutter 的 itemBuilder 模式将索引作为唯一判断依据,逻辑集中但略显抽象:
itemBuilder: (context, index) {
if (index == items.length) return loadMoreIndicator;
return normalItem;
}
鸿蒙 ArkUI 的 ForEach + 条件 ListItem 模式更接近"自然语言":
ForEach(items, item => { /* 正常项 */ })
if (hasMore) { /* 加载指示器 */ }
从可读性角度看,鸿蒙 ArkUI 的声明式写法更直观,尤其适合团队协作和代码审查。
7.3 学习曲线
对于有 Flutter 经验的开发者,迁移到鸿蒙 ArkUI 需要注意以下几点:
- 状态管理:从
setState()到@State装饰器,从"命令式触发更新"到"声明式响应更新" - 列表构建:从
itemBuilder回调到ForEach遍历,思维方式从"按索引构建"变为"按数据构建" - 滚动监听:从
ScrollController到onScrollIndex事件,从"手动控制"到"事件驱动" - 异步处理:从
Future/async-await到setTimeout/Promise,语法差异不大
整体而言,对于有 Flutter 或 React 经验的开发者,学习鸿蒙 ArkUI 的脚手架成本较低,通常 1-2 周即可上手。
八、总结
本文详细介绍了在鸿蒙 ArkUI 中实现列表底部加载更多指示器的完整方案,涵盖了从状态设计、数据加载、UI 构建到滚动监听的完整流程,并与 Flutter 的 ListView.builder 方案进行了全方位对比。
核心要点回顾:
- 状态驱动 UI:使用
@State装饰器管理分页状态,框架自动响应式更新 - 条件渲染指示器:在
List的ForEach之后追加条件ListItem,通过hasMoreData控制显示/隐藏 - 滚动提前触发:使用
onScrollIndex事件在滚动到倒数第 2 项时提前触发加载 - 加载锁防重复:
isLoadingMore标志位防止并发请求 - 搜索联动重置:搜索时重置分页状态,确保搜索与分页数据一致
这个模式适用于绝大多数需要分页加载的列表场景,包括但不限于:商品列表、文章列表、评论列表、好友列表、消息列表等。开发者可以根据实际需求,将本文的本地数据模拟替换为真实的 HTTP 请求,并扩展错误处理、重试机制、加载动画定制等高级功能。
九、参考资料
- HarmonyOS 开发者文档 - List 组件
- HarmonyOS 开发者文档 - LoadingProgress 组件
- HarmonyOS 开发者文档 - @State 装饰器
- Flutter 文档 - ListView.builder
- HarmonyOS 开发者文档 - 声明式 UI 开发指南
更多推荐



所有评论(0)