HarmonyOS7 列表流实战--------下拉刷新和触底加载这样写最顺手
文章目录
源码获取
如果你想一边对照文章一边实操,建议直接把示例工程拉到本地。项目 Git 地址:https://gitcode.com/HarmonyOS_Samples/CommonListFlows。
列表页最容易被低估的地方,不是布局,而是交互节奏。
页面静静躺在那里时,看起来谁都能写。真到用户开始下拉、猛滑、触底、回拉的时候,问题一下子全冒出来了: 刷新什么时候结束,加载更多什么时候停,没有更多数据怎么收尾,状态到底该由谁来管。
HomePage 这页给的是一套很适合新手起步的写法。它没有上来就接接口,也没有塞一堆状态,而是先把最小闭环跑顺。这个思路我很认可。

先别急着接接口,先把状态闭环写对
很多人一做列表交互就急着想后端分页、接口协议、错误码处理。
这些当然重要,但如果你现在连刷新和加载更多的基本状态流转都没想清楚,接口接进来只会让页面更乱。

这个项目最聪明的一点,就是先把第一版交互写成一个能跑通的闭环。
第一版真的不用一堆状态,两个就够
项目里核心只用了这两个变量:
@State noMoreData: boolean = false
@State isRefreshing: boolean = false
它们的职责非常明确:
isRefreshing管“现在是不是处于刷新中”noMoreData管“后面还有没有数据可拿”
这就是一个非常典型的最小解。
很多新手一开始会写出 loading、refreshing、isFetch、requesting、hasMore、finished 一长串状态,最后连自己都分不清谁管谁。第一版真没必要那么复杂。
下拉刷新最关键的不是 API,而是顺序
页面把 List 包在了 Refresh 组件里:
Refresh({ refreshing: $$this.isRefreshing }) {
List({ space: 12 }) {
// 列表内容
}
}
.onRefreshing(() => {
this.isRefreshing = true
setTimeout(() => {
this.scenicSpotArray = ['演示景点 A', '演示景点 B', '演示景点 C', '演示景点 D', '演示景点 E']
this.noMoreData = false
this.isRefreshing = false
}, 2000)
})
如果你是第一次写刷新逻辑,我更建议你盯流程,不要死盯语法:
- 用户下拉。
- 组件进入刷新态。
- 刷新逻辑开始执行。
- 数据被重置或覆盖。
- 刷新结束,状态关掉。

这就是刷新最基本的闭环。
写列表交互时,闭环比花样重要得多。闭环不对,再漂亮的页面也会显得很假。
为什么刷新时一定要顺手把 noMoreData 重置掉
这个小动作很容易被忽略,但非常像真实业务思路。
如果你之前已经滚到了“没有更多数据”,那说明旧列表生命周期已经走到底了。现在用户重新下拉刷新,等于你重新拿了一批新数据。这个时候旧的“到底了”状态理应失效。
所以项目里会这样写:
this.noMoreData = false
如果你漏了这一步,页面很容易出现一种很怪的感觉: 用户刚刷新完,还没开始往下看,底部逻辑已经默认“后面啥都没了”。这种体验特别割裂。
触底加载更多,核心也不是难,而是别写散
项目直接用 List 的 onReachEnd() 来接触底逻辑:
.onReachEnd(() => {
if (this.scenicSpotArray.length >= 20) {
this.noMoreData = true
return
}
setTimeout(() => {
this.scenicSpotArray.push('演示景点 ' + (this.scenicSpotArray.length + 1))
}, 500)
})
这个实现很适合小白,因为它把事情说得非常直白:
- 如果数据够多了,就别再装作还能加载,直接标记到底。
- 如果还没到底,就继续往数组后面追加。
你以后真接接口时,把 setTimeout() 换成接口请求就行,整体思路不需要推倒重来。
底部加载区为什么最好就待在 List 里面
项目专门把底部反馈做成了一个单独的 ListItem:
ListItem() {
Row() {
if (!this.noMoreData) {
LoadingProgress()
}
Text(this.noMoreData ? $r('app.string.no_more_data') : $r('app.string.loading_more'))
}
}

这个处理我很推荐。
因为“加载中”和“已经到底”本质上也是列表的一部分。它们就应该跟着列表一起出现、一起滚动、一起离开。
你要是把这块硬浮在页面外层,早晚会和滚动区域、底部安全区或者布局权重打架。
这页为什么故意不用真实接口
因为这篇示例的重点根本不是后端联调,而是交互节奏。
所以作者才用 setTimeout() 模拟网络延迟。这么做一点也不丢人,反而很适合教学。对小白来说,先把“刷新怎么开始、怎么结束、怎么改数据、怎么收状态”看顺,比一上来就盯接口返回值有用得多。
真要接接口,哪几步最值得补上
如果你后面准备把它改成真实项目,我建议你至少补这两类保护。
第一类: 刷新保护
- 刷新时清掉旧分页信息
- 重新请求第一页
- 用新结果覆盖原数组
- 无论成功失败,都记得关掉
isRefreshing
第二类: 加载更多保护
- 增加一个
isLoadingMore,避免连续触底重复请求 - 接口没数据时,把
noMoreData置为true - 请求失败时,别偷偷吞掉状态,至少给页面一个可恢复的结果
当前示例没把这些都展开,是为了让代码别太长。但你自己上手做项目时,最好别省。
这页手感为什么还不错
除了刷新和加载本身,项目还顺手配了几项挺重要的属性:
.scrollBar(BarState.Off)
.sticky(StickyStyle.Header)
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
.edgeEffect(EdgeEffect.Spring, { alwaysEnabled: true })
这些配置单看不起眼,放一起就很有用:
- 滚动条藏掉,界面会更干净
- 分组标题能吸顶,长列表更有层次
- 底部安全区不容易被压住
- 滑到边缘时手感更像正式产品
很多页面明明结构差不多,但看起来就是一个像 demo、一个像成品,差别往往就在这些细节上。
我更担心你踩的,不是难点,而是这些小坑
刷新结束了,却忘了关刷新状态
这种问题特别常见。结果就是转圈一直转,像页面卡住了一样。
底部文案变了,数据却没变
用户会看到“正在加载更多”,但内容一行不长,这种体验比没有加载提示还糟。
触底触发太快,重复加了好几次
真项目里很常见,尤其是滑得快的时候。所以我前面才一直强调 isLoadingMore 迟早要补。
刷新以后,旧分页状态还留着
这会让第一页和后面的页码逻辑缠在一起,数据顺序很容易变怪。
现在就能做的两个练手动作
你不用等接口,先做两个本地改动就够了:
- 把“最多加载 20 条”改成“最多加载 10 条”,观察
noMoreData什么时候翻转。 - 刷新时把列表替换成另一组演示名称,确认页面是不是会按预期重绘。
这两个动作很小,但特别能帮你建立状态感。
最后一句
下拉刷新和加载更多,表面上只是两个常见功能,实际上它们最考验你对“状态”和“数据”关系的理解。
这一页如果你练顺了,收获不只是会写两个 API,而是开始知道一页真正可用的列表,应该怎么把交互节奏接起来。这个感觉一旦有了,你后面写内容流页面会稳很多。
更多推荐


所有评论(0)