鸿蒙原生 ArkTS 布局精讲:List 下拉刷新(PullToRefresh)实战


在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

一、前言

在移动端应用中,下拉刷新(PullToRefresh) 是最基础也最常用的交互模式之一。无论新闻资讯的首页列表、社交动态的时间线,还是电商商品流,下拉刷新都扮演着"让用户主动获取最新数据"的关键角色。

HarmonyOS NEXT(API 24)在 ArkUI 框架中提供了原生的下拉刷新解决方案——Refresh 组件。本文将从一个完整的实战示例出发,深入解析鸿蒙原生下拉刷新的实现原理、API 细节与避坑指南。


二、基础知识

2.1 什么是 Refresh 组件?

Refresh 是 ArkUI 框架提供的一个容器组件(API 24 中为内置组件,无需 import),专门用于实现下拉刷新。它通过包裹可滚动内容区域(ListScrollGrid 等),在用户下拉时显示加载指示器,并在松手时触发刷新回调。

工作流程如下:

手指下拉 → Refresh 检测偏移量 → 指示器跟随移动
    → 超过阈值松手 → 触发 onRefreshing() → 执行异步刷新
    → 刷新完成 → refreshing = false → 指示器消失,列表复位

2.2 Refresh 与 List 的关系

  • Refresh容器List内容——Refresh 包裹 List
  • Refresh 负责手势识别与偏移跟踪,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);
  }

关键:刷新完成必须将 isRefreshingfalse,否则指示器永远旋转,用户无法再次刷新。

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

三个参数:

  1. 数据源 Array<T>——@State 装饰时支持响应式更新
  2. 项生成器 (item, index?) => void——为每个元素生成 UI 节点
  3. 键生成器 (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 中 ListRefresh 是语言内建组件,全局可用:

// ❌ 错误: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 组件封装了完整的触摸事件处理:

  1. 触摸拦截:用户在内容顶部边缘下拉时,Refresh 拦截触摸
  2. 偏移跟随:手指下拉,内容跟随偏移,指示器逐步显现
  3. 阈值判断:偏移超过 refreshOffset(默认 64vp)时,进入就绪状态
  4. 触发刷新:松手时偏移≥阈值→调用 onRefreshing(),指示器旋转,内容弹性回弹
  5. 完成复位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 {
    // 刷新结束:统计耗时
  }
}

十、总结

通过本文,我们完成了以下学习目标:

  1. Refresh 组件的使用Refresh({ refreshing }) { 内容 } .onRefreshing() .refreshOffset()
  2. List + ForEach 的配合ForEach 是渲染指令,不是组件,不可链式调用
  3. API 24 特性:内置组件无需 import,事件名为 onRefreshing
  4. 完整刷新流程:下拉 → 偏移跟踪 → 阈值判断 → 触发刷新 → 完成复位

下拉刷新看似简单,但涉及手势识别、状态管理、异步编程与 UI 动画等多个知识领域。掌握了 Refresh 组件,你就掌握了鸿蒙原生应用中最高频的交互模式之一。


附录:代码索引

文件 路径
页面入口 entry/src/main/ets/pages/Index.ets
Ability 入口 entry/src/main/ets/entryability/EntryAbility.ets
模块配置 entry/src/main/module.json5
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐