下拉刷新与上拉加载更多——从原生 Refresh 到高性能长列表工程化
文章目录

每日一句正能量
“即使触不到满天星辰,也要奔跑在萤火之森。”
梦想或许遥不可及,但过程本身就值得奔赴。星辰是远方的理想,萤火是当下的微光——即便无法摘取天上的光芒,也要在身边的点点光亮中全力奔跑。真正的热爱,不是只看向终点,而是享受脚下的每一步。
摘要
在移动端应用中,下拉刷新(Pull-to-Refresh) 与 上拉加载更多(Load-More) 是长列表交互的两大基石。前者让用户获取最新数据,后者让无限内容得以渐进呈现。HarmonyOS ArkTS 通过 Refresh 组件与 List.onReachEnd 提供了声明式的一等公民支持,但要在生产环境中实现零抖动、零白屏、高复用的刷新体验,仍需深入理解其状态机、数据懒加载机制与组件复用策略。
本文基于 HarmonyOS 6(API 23),从 Refresh 组件的状态机出发,系统讲解下拉刷新的完整实现链路;结合 LazyForEach + IDataSource 构建高性能长列表;通过 onReachEnd 与防抖机制实现稳健的上拉加载;最后给出空状态、错误重试、骨架屏等生产级优化方案,形成一套可落地的长列表工程化范式。
一、Refresh 组件:下拉刷新的状态机
Refresh 是 HarmonyOS NEXT 提供的下拉刷新容器组件,用于为 List、Scroll、Grid、WaterFlow 等可滚动内容添加下拉刷新能力。与手动监听 onTouch 手势实现刷新不同,Refresh 将手势识别、阈值判定、状态切换、动画反馈全部封装在框架层,开发者只需关注业务数据逻辑。
1.1 核心 API 速览
| 属性/回调 | 类型 | 说明 |
|---|---|---|
refreshing |
boolean |
双向绑定刷新状态,控制 Loading 动画显示与隐藏 |
onRefreshing() |
() => void |
松手且超过阈值后触发,执行业务刷新逻辑 |
onStateChange() |
(status) => void |
监听 Refresh 内部状态流转,可用于自定义头部动画 |
builder |
CustomBuilder |
自定义刷新头部 UI(可选,默认使用系统样式) |
1.2 状态机流转
Refresh 组件内部维护了一套完整的状态机,开发者无需干预其流转,但理解其机制有助于排查异常行为:

| 状态 | 枚举值 | 含义 |
|---|---|---|
| Inactive | RefreshStatus.Inactive |
初始闲置状态,手指未触碰 |
| Drag | RefreshStatus.Drag |
手指下拉中,距离小于触发阈值 |
| OverDrag | RefreshStatus.OverDrag |
下拉距离超过阈值,松手即触发刷新 |
| Refresh | RefreshStatus.Refresh |
正在执行刷新,显示 Loading 动画 |
| Done | RefreshStatus.Done |
刷新完成,执行回弹动画后回到 Inactive |
关键认知:onRefreshing 回调在 OverDrag → Refresh 的跃迁瞬间触发,而非手指按下时。这意味着框架已经帮开发者完成了「是否超过阈值」的判断,无需手动计算拖拽距离。
二、基础实现:Refresh + List 组合
2.1 最小可用示例
以下代码展示了下拉刷新与上拉加载的最小完整实现,覆盖数据模型、状态管理、事件处理的全链路:
interface NewsItem {
id: string;
title: string;
summary: string;
time: string;
imageUrl: string;
}
@Entry
@Component
struct NewsListPage {
@State dataList: NewsItem[] = [];
@State isRefreshing: boolean = false;
@State isLoading: boolean = false;
@State hasMore: boolean = true;
private pageNum: number = 1;
private readonly pageSize: number = 10;
aboutToAppear(): void {
this.loadData(true);
}
// 数据请求(模拟网络)
private async fetchData(page: number, isRefresh: boolean): Promise<NewsItem[]> {
return new Promise((resolve) => {
setTimeout(() => {
const items: NewsItem[] = [];
const base = (page - 1) * this.pageSize;
for (let i = 1; i <= this.pageSize; i++) {
const idx = base + i;
items.push({
id: `news_${idx}`,
title: `新闻标题 ${idx}:HarmonyOS 生态持续繁荣`,
summary: `这是第 ${idx} 条新闻的摘要内容,展示 ArkTS 声明式开发范式的强大能力...`,
time: `2026-08-${String(idx % 30 + 1).padStart(2, '0')}`,
imageUrl: ''
});
}
resolve(items);
}, 800);
});
}
// 加载数据(刷新或加载更多)
private async loadData(isRefresh: boolean): Promise<void> {
if (isRefresh) {
this.isRefreshing = true;
this.pageNum = 1;
} else {
if (this.isLoading || !this.hasMore) return;
this.isLoading = true;
}
try {
const data = await this.fetchData(this.pageNum, isRefresh);
if (isRefresh) {
this.dataList = data; // 刷新:替换数据
} else {
this.dataList = this.dataList.concat(data); // 加载更多:追加数据
}
this.hasMore = data.length >= this.pageSize;
this.pageNum++;
} catch (error) {
console.error('数据加载失败:', error);
} finally {
this.isRefreshing = false;
this.isLoading = false;
}
}
@Builder
newsItemBuilder(item: NewsItem) {
Row() {
Column() {
Text(item.title)
.fontSize(15)
.fontWeight(FontWeight.Medium)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.width('100%')
Text(item.summary)
.fontSize(13)
.fontColor('#666666')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ top: 6 })
.width('100%')
Text(item.time)
.fontSize(11)
.fontColor('#999999')
.margin({ top: 6 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.margin({ right: 12 })
Image($r('app.media.ic_news_placeholder'))
.width(80)
.height(60)
.borderRadius(4)
.objectFit(ImageFit.Cover)
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
}
@Builder
loadMoreFooter() {
Row() {
if (this.isLoading) {
LoadingProgress()
.width(20)
.height(20)
.color('#999999')
Text('正在加载...')
.fontSize(13)
.fontColor('#999999')
.margin({ left: 8 })
} else if (!this.hasMore) {
Text('—— 已经到底了 ——')
.fontSize(13)
.fontColor('#CCCCCC')
}
}
.width('100%')
.height(50)
.justifyContent(FlexAlign.Center)
}
build() {
Column() {
Row() {
Text('新闻资讯')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
}
.width('100%')
.height(56)
.padding({ left: 16 })
.backgroundColor('#2C3E50')
Refresh({ refreshing: $$this.isRefreshing }) {
List({ space: 1 }) {
ForEach(this.dataList, (item: NewsItem) => {
ListItem() {
this.newsItemBuilder(item)
}
}, (item: NewsItem) => item.id) // key 生成器:业务唯一 ID
// 底部加载更多
ListItem() {
this.loadMoreFooter()
}
}
.width('100%')
.layoutWeight(1)
.divider({ strokeWidth: 1, color: '#F0F0F0' })
.edgeEffect(EdgeEffect.Spring)
.onReachEnd(() => {
this.loadData(false); // 触发加载更多
})
.nestedScroll({
scrollForward: NestedScrollMode.SELF_FIRST,
scrollBackward: NestedScrollMode.SELF_FIRST
})
}
.onRefreshing(() => {
this.loadData(true); // 触发下拉刷新
})
.onStateChange((status: RefreshStatus) => {
console.info(`Refresh 状态变化: ${status}`);
})
.layoutWeight(1)
.width('100%')
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
}
2.2 代码要点解析
① 双向绑定语法 $$this.isRefreshing
Refresh 组件的 refreshing 属性支持 $$ 双向绑定。当刷新完成时,框架会自动将 isRefreshing 重置为 false,无需开发者手动置位。若使用单向绑定 : this.isRefreshing,则需在 finally 块中手动关闭。
② onReachEnd 防抖保护
List.onReachEnd 在滚动到底部时触发,但快速滑动或列表项高度不均时可能连续触发多次。必须通过 isLoading 状态锁和 hasMore 边界判断实现防抖:
if (this.isLoading || !this.hasMore) return;
③ 数据更新原则:引用替换
ArkTS 的响应式系统基于引用比较。直接修改数组元素(this.dataList[0].title = 'xxx')不会触发 UI 更新,必须通过重新赋值数组引用实现:
// 刷新:替换引用
this.dataList = data;
// 加载更多:concat 生成新引用
this.dataList = this.dataList.concat(data);
三、架构全景:组件层级与事件流

从架构视角看,下拉刷新与上拉加载是一个**「事件驱动 + 状态管理 + 数据分层」**的协同系统:
- 事件层:
Refresh负责下拉手势识别,List.onReachEnd负责触底检测,两者独立触发、互不干扰。 - 状态层:
isRefreshing、isLoading、hasMore三个布尔状态构成互斥锁,防止并发请求。 - 数据层:刷新操作执行「替换」(
page = 1),加载更多执行「追加」(page++),数据流向清晰可预测。
四、高性能长列表:LazyForEach + IDataSource
当列表数据量超过百条时,ForEach 会在页面初始化时一次性创建所有组件节点,导致内存峰值飙升和启动耗时增加。HarmonyOS 提供了 LazyForEach 实现按需渲染——仅创建可视区域及其缓冲区的组件,滑出视口时自动销毁。
4.1 IDataSource 完整实现
export class NewsDataSource implements IDataSource {
private list: NewsItem[] = [];
private listeners: DataChangeListener[] = [];
constructor(list: NewsItem[] = []) {
this.list = list;
}
totalCount(): number {
return this.list.length;
}
getData(index: number): NewsItem {
return this.list[index];
}
// 注册监听器(框架调用)
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
this.listeners.push(listener);
}
}
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) {
this.listeners.splice(pos, 1);
}
}
// 业务方法:刷新数据
refreshData(newList: NewsItem[]): void {
this.list = newList;
this.notifyDataReload();
}
// 业务方法:追加数据
appendData(newList: NewsItem[]): void {
const start = this.list.length;
this.list = this.list.concat(newList);
this.notifyDataAdd(start, newList.length);
}
// 通知框架数据已重新加载
private notifyDataReload(): void {
this.listeners.forEach(listener => {
listener.onDataReloaded();
});
}
// 通知框架指定范围数据新增
private notifyDataAdd(index: number, count: number): void {
this.listeners.forEach(listener => {
listener.onDataAdd(index, count);
});
}
}
4.2 LazyForEach 使用规范
// 页面中使用 LazyForEach
@State newsData: NewsDataSource = new NewsDataSource();
// 在 List 中替换 ForEach
List({ space: 1 }) {
LazyForEach(this.newsData, (item: NewsItem) => {
ListItem() {
this.newsItemBuilder(item)
}
}, (item: NewsItem) => item.id) // key 必须用业务唯一 ID
}
.cachedCount(3) // 预加载屏幕外 3 条,防止快速滚动白屏

4.3 LazyForEach 核心规则
| 规则 | 说明 | 违规后果 |
|---|---|---|
| key 唯一性 | 必须使用业务唯一 ID(如 item.id),禁止使用 index |
删除/插入时组件复用错位,UI 闪烁 |
| 禁止重赋值 dataSource | 不能执行 this.newsData = new NewsDataSource() |
LazyForEach 监听器丢失,UI 不更新 |
| 必须通过监听器更新 | 调用 onDataAdd / onDataReloaded 等方法 |
直接修改内部数组框架感知不到 |
| 单根组件 | LazyForEach 的 itemBuilder 必须且只能返回一个根组件 |
编译报错或运行时异常 |
| 配合 @Reusable | 列表项组件添加 @Reusable 装饰器 |
组件节点复用,创建耗时降低约 69% |
五、生产级优化:空状态、错误重试与骨架屏
基础实现只能应对「理想网络 + 正常数据」的场景。生产环境中,必须处理空数据、网络错误、加载过渡等边界情况。
5.1 状态机扩展
enum ListState {
IDLE = 'idle', // 初始/正常浏览
REFRESHING = 'refreshing', // 下拉刷新中
LOADING_MORE = 'loading_more', // 上拉加载中
EMPTY = 'empty', // 无数据
ERROR = 'error', // 加载失败
NO_MORE = 'no_more' // 已加载全部
}
@State listState: ListState = ListState.IDLE;
5.2 空状态与错误重试
@Builder
emptyView() {
Column() {
Image($r('app.media.ic_empty'))
.width(120)
.height(120)
.opacity(0.5)
Text('暂无数据')
.fontSize(16)
.fontColor('#999999')
.margin({ top: 16 })
Button('重新加载')
.margin({ top: 24 })
.onClick(() => {
this.loadData(true);
})
}
.width('100%')
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
}
@Builder
errorView() {
Column() {
Image($r('app.media.ic_error'))
.width(100)
.height(100)
.opacity(0.5)
Text('加载失败,请检查网络')
.fontSize(15)
.fontColor('#E74C3C')
.margin({ top: 12 })
Button('点击重试')
.type(ButtonType.Capsule)
.margin({ top: 20 })
.onClick(() => {
this.loadData(true);
})
}
.width('100%')
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
}
5.3 骨架屏(Skeleton Loading)
骨架屏在数据加载前展示占位动画,可显著降低用户感知等待时间:
@Builder
skeletonItem() {
Row() {
Column() {
Rectangle() // 模拟标题占位
.width('70%')
.height(16)
.fill('#E8E8E8')
.borderRadius(4)
Rectangle() // 模拟摘要占位
.width('90%')
.height(12)
.fill('#F0F0F0')
.borderRadius(4)
.margin({ top: 10 })
}
.layoutWeight(1)
.margin({ right: 12 })
Rectangle() // 模拟图片占位
.width(80)
.height(60)
.fill('#E8E8E8')
.borderRadius(4)
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
}
// 使用 shimmer 动画
@Builder
shimmerSkeleton() {
Column() {
ForEach([1, 2, 3, 4, 5], () => {
this.skeletonItem()
})
}
.width('100%')
.opacity(0.7)
.animation({
duration: 1500,
iterations: -1,
curve: Curve.EaseInOut,
playMode: PlayMode.Alternate
})
}
六、进阶:与吸顶效果的协同(承接前序文章)
在第一百一十二篇文章中,我们深入探讨了吸顶效果的三种实现方案。当吸顶头部与下拉刷新共存时,需要特别注意事件分发优先级和滚动偏移量计算。
6.1 吸顶 + 下拉刷新的嵌套策略
当页面顶部存在吸顶 TabBar(如电商分类页),且整个页面支持下拉刷新时,推荐采用以下嵌套结构:
// 外层 Refresh 包裹整个页面内容
Refresh({ refreshing: $$this.isRefreshing }) {
Scroll() {
Column() {
// Banner 区域(滚动时推出)
BannerView()
// 吸顶 TabBar
StickyTabBar()
// 内层列表(带独立上拉加载)
List() {
LazyForEach(this.dataSource, ...)
}
.onReachEnd(() => this.loadMore())
}
}
}
.onRefreshing(() => this.refreshAll())
6.2 关键配置
| 场景 | 配置 | 说明 |
|---|---|---|
| 外层 Scroll + 内层 List | 外层 nestedScroll: PARENT_FIRST |
下拉时外层 Scroll 先响应,保证 Refresh 手势不被拦截 |
| 吸顶 TabBar 高度固定 | 预留占位组件 | 防止吸顶瞬间内容跳动 |
| 刷新重置页码 | onRefreshing 中 page = 1 |
刷新后从第一页重新加载 |
七、性能优化清单
| 优化项 | 实现方式 | 预期收益 |
|---|---|---|
| 懒加载 | LazyForEach + IDataSource |
仅渲染可视区,降低 80%+ 内存 |
| 组件复用 | @Reusable 装饰列表项 |
组件创建耗时降低约 69% |
| 预加载缓冲 | cachedCount(3~5) |
快速滚动无白屏 |
| 图片懒加载 | onVisibleAreaChange 中加载 |
减少首屏内存和流量 |
| 分页控制 | pageSize 控制在 10~20 条 |
平衡请求次数与单次数据量 |
| 防抖节流 | isLoading + hasMore 状态锁 |
防止重复请求和空转 |
| 引用更新 | concat / 重新赋值数组 |
确保响应式系统正确感知变化 |
八、完整实战效果

上图展示了三种典型状态:
- 下拉刷新中:手指下拉超过阈值,
Refresh头部显示 Loading 动画,业务层执行page = 1的数据请求。 - 正常浏览:
LazyForEach按需渲染可视区列表项,cachedCount预加载缓冲区内容,滚动流畅无白屏。 - 上拉加载更多:滚动到底触发
onReachEnd,底部显示加载提示,数据追加到列表尾部。
九、总结
本文围绕 HarmonyOS 6(API 23)的下拉刷新与上拉加载,构建了从基础实现到生产优化的完整技术体系:
- Refresh 状态机:理解
Inactive → Drag → OverDrag → Refresh → Done的流转机制,是排查刷新异常的基础。 - LazyForEach 工程化:
IDataSource的规范实现 + 业务唯一 key +@Reusable组件复用,是长列表性能的核心保障。 - 上拉加载稳健性:
onReachEnd配合isLoading/hasMore双锁机制,可有效防止重复请求和空数据异常。 - 生产级体验:空状态、错误重试、骨架屏三件套,是区分「Demo」与「产品」的关键细节。
- 与吸顶效果协同:通过合理的嵌套结构和
nestedScroll配置,下拉刷新可与吸顶头部无缝共存。
掌握本文所述的工程化范式,开发者即可在 HarmonyOS 应用中构建出媲美原生体验的高性能长列表。
转载自:https://blog.csdn.net/u014727709/article/details/163418917
欢迎 👍点赞✍评论⭐收藏,欢迎指正
更多推荐



所有评论(0)