【共创季稿事节】鸿蒙原生 ArkTS 布局精讲:List 下拉刷新(PullToRefresh)实战
鸿蒙原生 ArkTS 布局精讲:List 下拉刷新(PullToRefresh)实战



一、前言
在移动端应用中,下拉刷新(PullToRefresh) 是最基础也最常用的交互模式之一。无论新闻资讯的首页列表、社交动态的时间线,还是电商商品流,下拉刷新都扮演着"让用户主动获取最新数据"的关键角色。
HarmonyOS NEXT(API 24)在 ArkUI 框架中提供了原生的下拉刷新解决方案——Refresh 组件。本文将从一个完整的实战示例出发,深入解析鸿蒙原生下拉刷新的实现原理、API 细节与避坑指南。
二、基础知识
2.1 什么是 Refresh 组件?
Refresh 是 ArkUI 框架提供的一个容器组件(API 24 中为内置组件,无需 import),专门用于实现下拉刷新。它通过包裹可滚动内容区域(List、Scroll、Grid 等),在用户下拉时显示加载指示器,并在松手时触发刷新回调。
工作流程如下:
手指下拉 → Refresh 检测偏移量 → 指示器跟随移动
→ 超过阈值松手 → 触发 onRefreshing() → 执行异步刷新
→ 刷新完成 → refreshing = false → 指示器消失,列表复位
2.2 Refresh 与 List 的关系
Refresh是容器,List是内容——Refresh包裹ListRefresh负责手势识别与偏移跟踪,List负责数据驱动渲染- 两者通过
refreshing状态变量协作
三、完整示例代码
文件路径:entry/src/main/ets/pages/Index.ets
3.1 数据结构与数据生成
// 注意:不能使用 ListItem 作为接口名,会与内置组件冲突
interface DataItem {
id: number; // 唯一标识
title: string; // 标题
desc: string; // 描述
time: string; // 时间
type: string; // 分类标签
}
function generateMockData(startId: number, count: number): DataItem[] {
const tags = ['推荐', '热点', '科技', '财经', '体育', '娱乐', '军事', '教育'];
const data: DataItem[] = [];
for (let i = 0; i < count; i++) {
const id = startId + i;
data.push({
id, title: `示例条目 #${id}`,
desc: `第 ${id} 条模拟数据,演示 List + Refresh 的下拉刷新效果。`,
time: `2026-06-26 ${String(8 + i % 10).padStart(2,'0')}:${String(i*3%60).padStart(2,'0')}`,
type: tags[id % tags.length]
});
}
return data;
}
要点:使用 interface 而非 class,接口更轻量,适合纯数据模型。注意接口名避开内置组件名,否则编译器报错 Use unique names for types and namespaces。
3.2 组件与状态管理
@Entry
@Component
struct PullToRefreshDemo {
@State private dataList: DataItem[] = generateMockData(1, 20);
@State private isRefreshing: boolean = false;
private nextId: number = 21;
private readonly REFRESH_OFFSET: number = 80;
@State装饰器:数据变化自动触发 UI 重绘。dataList更新时ForEach自动重建列表;isRefreshing变化时加载指示器自动显示/隐藏- 非状态成员:
nextId(仅用于数据生成)和REFRESH_OFFSET(只读常量)无需@State
3.3 刷新回调
private onPullRefresh(): void {
this.isRefreshing = true; // ① 显示加载动画
setTimeout(() => { // 模拟网络延迟
const newData = generateMockData(this.nextId, 5);
this.nextId += 5;
this.dataList = [...newData, ...this.dataList]; // ② 头部插入新数据
this.isRefreshing = false; // ③ 结束刷新
}, 1500);
}
关键:刷新完成必须将 isRefreshing 置 false,否则指示器永远旋转,用户无法再次刷新。
3.4 UI 构建
build() {
Column() {
// 顶部标题栏
Row() {
Text('下拉刷新示例').fontSize(20).fontWeight(FontWeight.Bold)
}
.width('100%').height(56).justifyContent(FlexAlign.Center)
.backgroundColor(Color.White)
.shadow({ radius: 2, color: 'rgba(0,0,0,0.08)', offsetX: 0, offsetY: 1 })
// Refresh 下拉刷新容器
Refresh({ refreshing: this.isRefreshing }) {
List() {
ForEach(this.dataList, (item: DataItem, index?: number) => {
ListItem() {
Column() {
// 第一行:分类标签 + 时间
Row() {
Text(item.type).fontSize(12).fontColor(Color.White)
.backgroundColor('#FF6B81').borderRadius(8)
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
Blank()
Text(item.time).fontSize(12).fontColor('#999999')
}.width('100%')
// 第二行:标题
Text(item.title).fontSize(16).fontWeight(FontWeight.Medium)
.margin({ top: 8, bottom: 6 }).maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
// 第三行:描述
Text(item.desc).fontSize(14).fontColor('#666666')
.lineHeight(20).maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.width('100%').padding(16).backgroundColor(Color.White)
.borderRadius(12)
.shadow({ radius: 4, color: 'rgba(0,0,0,0.06)', offsetX: 0, offsetY: 2 })
}
.width('100%')
.margin({ top: index === 0 ? 0 : 8, left: 16, right: 16 })
}, (item: DataItem): string => item.id.toString())
}
.width('100%').height('100%')
}
.width('100%').height('100%')
.onRefreshing(() => { this.onPullRefresh(); })
.refreshOffset(this.REFRESH_OFFSET)
.onOffsetChange((offset: number) => {
console.info('[PullToRefresh] 偏移量: ' + offset + 'vp');
})
}
.width('100%').height('100%')
.backgroundColor('#F5F6FA')
}
}
布局层次:
Column → Row(标题栏) → Refresh → List → ForEach → ListItem × N
四、关键 API 详解
4.1 Refresh 组件
| 参数/方法 | 说明 |
|---|---|
refreshing: boolean |
必填。是否处于刷新状态 |
.onRefreshing(() => void) |
核心方法。下拉松手且偏移超阈值时触发。注意 API 24 名称为 onRefreshing(有 ing),不是 onRefresh |
.refreshOffset(vp: number) |
触发偏移阈值,默认 64vp |
.onOffsetChange(callback) |
下拉过程偏移回调,用于日志或自定义动画 |
4.2 ForEach 渲染指令
ForEach 不是组件,是渲染控制指令,不支持链式调用:
// ✅ 正确
List() {
ForEach(data, (item) => { ListItem() { ... } }, keyFn)
}
.width('100%')
// ❌ 错误:ForEachAttribute 没有 width 属性
List() {
ForEach(data, ...).width('100%')
}
三个参数:
- 数据源
Array<T>——@State装饰时支持响应式更新 - 项生成器
(item, index?) => void——为每个元素生成 UI 节点 - 键生成器
(item) => string——唯一 ID 优化 Diff,使用id.toString()而非索引
4.3 Blank() 弹性空间
Row() {
Text(item.type)
Blank() // 弹性空间,将两侧元素推到 Row 两端
Text(item.time)
}
相当于 Web Flex 中的 flex: 1 空白占位,比 Space() 更适合两端对齐。
五、API 24 版本特性与避坑
5.1 内置组件无需 import
API 24 中 List 和 Refresh 是语言内建组件,全局可用:
// ❌ 错误:Module has no exported member 'List'
import { List, Refresh } from '@kit.ArkUI';
// ✅ 直接使用
Refresh({ refreshing }) { List() { ... } }
5.2 事件名:onRefreshing 而非 onRefresh
// ❌ 编译错误
.onRefresh(() => { })
// ✅ 正确
.onRefreshing(() => { })
误用 onRefresh 时编译器会提示:Did you mean 'onRefreshing'?
5.3 接口名与内置组件冲突
// ❌ 编译错误:Use unique names for types and namespaces
interface ListItem { ... }
// ✅ 正确
interface DataItem { ... }
// 或 ItemModel, CardItem 等差异化名称
六、布局原理分析
6.1 Refresh 手势处理机制
Refresh 组件封装了完整的触摸事件处理:
- 触摸拦截:用户在内容顶部边缘下拉时,Refresh 拦截触摸
- 偏移跟随:手指下拉,内容跟随偏移,指示器逐步显现
- 阈值判断:偏移超过
refreshOffset(默认 64vp)时,进入就绪状态 - 触发刷新:松手时偏移≥阈值→调用
onRefreshing(),指示器旋转,内容弹性回弹 - 完成复位:
isRefreshing = false→ 指示器消失,内容复位
6.2 卡片样式设计
- 白色圆角卡片(
borderRadius: 12):隔离列表项,提升可读性 - 轻微阴影(
rgba(0,0,0,0.06)):增加层叠感 - 粉色标签(
#FF6B81):醒目品牌色 - 文本省略(
Ellipsis, maxLines):防止超长文本破坏布局 - 页面灰底(
#F5F6FA):与白色卡片形成对比
七、常见编译错误速查
| 错误信息 | 原因 | 解决 |
|---|---|---|
Use unique names for types and namespaces |
接口名 ListItem 与内置组件冲突 |
重命名为 DataItem |
Module has no exported member 'List' |
从 @kit.ArkUI import 了内置组件 |
删除 import |
Property 'width' does not exist on type 'ForEachAttribute' |
在 ForEach 上链式调用 .width() |
将属性移到 List 上 |
'}' expected |
常为前面类型错误导致的 AST 解析失败 | 从第一个错误开始排查 |
Did you mean 'onRefreshing'? |
使用了 onRefresh 而非 onRefreshing |
改为 onRefreshing |
八、性能优化与最佳实践
8.1 稳定 Key 生成器
// ✅ 推荐:item.id.toString() 稳定唯一
ForEach(data, gen, (item) => item.id.toString())
// ❌ 避免:索引作为 key,列表变化会导致全量重建
ForEach(data, gen, (item, i) => i.toString())
8.2 LazyForEach 替代 ForEach
超过 50 条数据时建议使用 LazyForEach,仅在项进入可视区域时创建 UI 节点,大幅降低内存占用。
8.3 合理设置 refreshOffset
- 默认 64vp 适合多数场景
- 顶部有固定元素(搜索栏/Tab 栏)时适当增大
- 不宜低于 40vp,避免与正常滚动混淆
8.4 异步任务安全清理
private isActive: boolean = true;
onPullRefresh(): void {
this.isRefreshing = true;
setTimeout(() => {
if (!this.isActive) return; // 组件已销毁,不再执行
// ...刷新逻辑
}, 1500);
}
aboutToDisappear(): void {
this.isActive = false;
}
九、扩展与进阶
9.1 上拉加载更多
List() {
ForEach(this.dataList, (item) => { ListItem() { ... } })
ListItem() {
Row() {
if (this.isLoadingMore) {
LoadingProgress().width(24).height(24)
Text('正在加载...').margin({ left: 8 })
} else {
Text('上拉加载更多')
}
}.width('100%').height(50).justifyContent(FlexAlign.Center)
}
}
.onReachEnd(() => { this.loadMoreData(); })
9.2 @Watch 监控刷新状态
@State @Watch('onStateChange') private isRefreshing: boolean = false;
private onStateChange(): void {
if (this.isRefreshing) {
// 刷新开始:埋点 / 日志
} else {
// 刷新结束:统计耗时
}
}
十、总结
通过本文,我们完成了以下学习目标:
- Refresh 组件的使用:
Refresh({ refreshing }) { 内容 } .onRefreshing() .refreshOffset() - List + ForEach 的配合:
ForEach是渲染指令,不是组件,不可链式调用 - API 24 特性:内置组件无需 import,事件名为
onRefreshing - 完整刷新流程:下拉 → 偏移跟踪 → 阈值判断 → 触发刷新 → 完成复位
下拉刷新看似简单,但涉及手势识别、状态管理、异步编程与 UI 动画等多个知识领域。掌握了 Refresh 组件,你就掌握了鸿蒙原生应用中最高频的交互模式之一。
附录:代码索引
| 文件 | 路径 |
|---|---|
| 页面入口 | entry/src/main/ets/pages/Index.ets |
| Ability 入口 | entry/src/main/ets/entryability/EntryAbility.ets |
| 模块配置 | entry/src/main/module.json5 |
更多推荐




所有评论(0)