源码获取

如果你想一边对照文章一边实操,建议直接把示例工程拉到本地。项目 Git 地址:https://gitcode.com/HarmonyOS_Samples/CommonListFlows

列表页最容易被低估的地方,不是布局,而是交互节奏。

页面静静躺在那里时,看起来谁都能写。真到用户开始下拉、猛滑、触底、回拉的时候,问题一下子全冒出来了: 刷新什么时候结束,加载更多什么时候停,没有更多数据怎么收尾,状态到底该由谁来管。

HomePage 这页给的是一套很适合新手起步的写法。它没有上来就接接口,也没有塞一堆状态,而是先把最小闭环跑顺。这个思路我很认可。

先别急着接接口,先把状态闭环写对

很多人一做列表交互就急着想后端分页、接口协议、错误码处理。

这些当然重要,但如果你现在连刷新和加载更多的基本状态流转都没想清楚,接口接进来只会让页面更乱。

A technical architecture diagram illustrating the

这个项目最聪明的一点,就是先把第一版交互写成一个能跑通的闭环。

第一版真的不用一堆状态,两个就够

项目里核心只用了这两个变量:

@State noMoreData: boolean = false
@State isRefreshing: boolean = false

它们的职责非常明确:

  • isRefreshing 管“现在是不是处于刷新中”
  • noMoreData 管“后面还有没有数据可拿”

这就是一个非常典型的最小解。

很多新手一开始会写出 loadingrefreshingisFetchrequestinghasMorefinished 一长串状态,最后连自己都分不清谁管谁。第一版真没必要那么复杂。

下拉刷新最关键的不是 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)
})

如果你是第一次写刷新逻辑,我更建议你盯流程,不要死盯语法:

  1. 用户下拉。
  2. 组件进入刷新态。
  3. 刷新逻辑开始执行。
  4. 数据被重置或覆盖。
  5. 刷新结束,状态关掉。

A step-by-step flowchart detailing the Pull-to-Ref

这就是刷新最基本的闭环。

写列表交互时,闭环比花样重要得多。闭环不对,再漂亮的页面也会显得很假。

为什么刷新时一定要顺手把 noMoreData 重置掉

这个小动作很容易被忽略,但非常像真实业务思路。

如果你之前已经滚到了“没有更多数据”,那说明旧列表生命周期已经走到底了。现在用户重新下拉刷新,等于你重新拿了一批新数据。这个时候旧的“到底了”状态理应失效。

所以项目里会这样写:

this.noMoreData = false

如果你漏了这一步,页面很容易出现一种很怪的感觉: 用户刚刷新完,还没开始往下看,底部逻辑已经默认“后面啥都没了”。这种体验特别割裂。

触底加载更多,核心也不是难,而是别写散

项目直接用 ListonReachEnd() 来接触底逻辑:

.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'))
  }
}

An ink-style comparison illustration contrasting t

这个处理我很推荐。

因为“加载中”和“已经到底”本质上也是列表的一部分。它们就应该跟着列表一起出现、一起滚动、一起离开。

你要是把这块硬浮在页面外层,早晚会和滚动区域、底部安全区或者布局权重打架。

这页为什么故意不用真实接口

因为这篇示例的重点根本不是后端联调,而是交互节奏。

所以作者才用 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,而是开始知道一页真正可用的列表,应该怎么把交互节奏接起来。这个感觉一旦有了,你后面写内容流页面会稳很多。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐