鸿蒙新特性:Refresh 下拉刷新——构建新闻资讯流与状态管理
下拉刷新是移动应用中最基础也最高频的交互之一。从微博到微信,从新闻客户端到电商应用,下拉刷新几乎无处不在——它让用户以最自然的手势获取最新内容,而无需寻找任何按钮。HarmonyOS NEXT ArkUI 提供了 Refresh 组件,将下拉刷新的手势识别、动画反馈和状态管理封装为一个开箱即用的容器。
但一个完整的"新闻资讯流"远不止下拉刷新:分类筛选让用户聚焦感兴趣的内容、收藏标记帮助用户沉淀优质文章、已读/未读状态让用户追踪浏览进度。本文将深入讲解 Refresh 组件与 List 的协作模式,并构建一个完整的"新闻资讯"应用——支持下拉刷新、分类筛选、收藏标记和已读管理。
关键词:HarmonyOS、ArkUI、Refresh、下拉刷新、新闻资讯、分类筛选、收藏
一、Refresh 组件 API
1.1 基本用法
Refresh 是一个容器组件,它的子组件必须是可滚动的(如 List、Scroll、Grid 等)。当用户下拉超过阈值时,触发刷新回调:
@State refreshing: boolean = false;
Refresh({ refreshing: $$this.refreshing }) {
List() {
ForEach(this.data, (item: NewsItem) => {
ListItem() { /* 列表项 */ }
})
}
}
.onRefreshing(() => {
this.refreshing = true;
// 执行刷新逻辑...
setTimeout(() => { this.refreshing = false; }, 1500);
})
核心属性:
- refreshing:通过
$$双向绑定一个@State boolean,控制刷新指示器的显示与隐藏 - onRefreshing:下拉触发刷新时的回调函数
1.2 双向绑定的含义
$$ 是 ArkUI 的双向绑定语法。与单向绑定 @State(父传子)不同,$$ 允许子组件(Refresh)在特定时机修改父组件的状态变量。具体到 Refresh:
- 用户下拉时,Refresh 内部开始动画
- 下拉距离超过阈值并松手后,Refresh 调用
onRefreshing回调 - 开发者在回调中设置
this.refreshing = true显示加载指示器 - 数据加载完成后,开发者设置
this.refreshing = false隐藏指示器
这种设计意味着开发者完全掌控刷新的生命周期——Refresh 组件只负责手势检测和动画,数据加载的时机和逻辑由开发者决定。
1.3 一个容易被忽略的细节
Refresh 组件的刷新回调名称是 onRefreshing,而非 onRefresh。这是 ArkUI 的命名惯例——onRefreshing 描述的是"正在刷新"这个动作,而不仅仅是"触发刷新"这个瞬间事件。如果你从其他框架(如 Flutter 的 onRefresh)迁移过来,这个差异需要特别注意。
二、新闻资讯流的整体设计
2.1 页面架构
本文 Demo 构建一个"新闻资讯"应用,模拟主流新闻客户端的信息流体验:
RefreshPage
├── 标题栏 — "新闻资讯" + 未读数徽标
├── 分类标签栏 — 横向滚动的分类选项卡(带未读计数)
├── 工具栏 — 结果计数 + 收藏筛选 + 全部已读
├── 下拉刷新区域(Refresh 组件)
│ └── List
│ ├── 空状态提示(无结果时)
│ └── 新闻卡片列表
│ ├── 分类标签 + 来源 + NEW 标记 + 时间 + 收藏星标
│ ├── 标题(已读/未读 粗细不同)
│ └── 摘要(最多 2 行)
└── 底部空白
2.2 数据结构
class NewsItem {
id: number;
title: string; // 标题
summary: string; // 摘要
source: string; // 来源
time: string; // 发布时间(相对时间)
category: string; // 分类(科技 / 财经 / 体育 / 娱乐)
bookmarked: boolean; // 是否收藏
read: boolean; // 是否已读
}
这里将 NewsItem 定义为 class 而非 interface,原因在于 ArkTS 不支持对象展开运算符(...item)。当需要在数组中"修改"某个对象的某个字段时(如将 bookmarked 置为 true),不能使用 { ...item, bookmarked: true },而必须显示创建一个新对象。为 NewsItem 添加工厂方法可以大幅简化这一操作:
class NewsItem {
// ... 属性声明 ...
constructor(id: number, title: string, summary: string, source: string,
time: string, category: string, bookmarked: boolean, read: boolean) {
// ... 属性赋值 ...
}
withBookmark(v: boolean): NewsItem {
return new NewsItem(this.id, this.title, this.summary, this.source,
this.time, this.category, v, this.read);
}
withRead(v: boolean): NewsItem {
return new NewsItem(this.id, this.title, this.summary, this.source,
this.time, this.category, this.bookmarked, v);
}
}
withBookmark 和 withRead 方法返回一个"除指定字段外完全相同"的新对象,配合 Array.map() 使用:
// 收藏/取消收藏:用 withBookmark 替代展开运算符
this.allNews = this.allNews.slice().map((item: NewsItem) => {
if (item.id === id) return item.withBookmark(!item.bookmarked);
return item;
});
这个模式是 ArkTS 中处理"不可变更新"的标准做法——通过 slice() 创建数组新引用,通过 map() + 工厂方法创建新的对象引用,触发 ArkUI 的状态感知和 UI 重绘。
2.3 12 篇预设新闻
Demo 提供了 12 篇新闻,覆盖四大分类:
| 新闻标题 | 分类 | 来源 |
|---|---|---|
| HarmonyOS NEXT 正式发布,全场景体验再升级 | 科技 | 华为官方 |
| 全球股市震荡:科技板块领跌,避险资产受追捧 | 财经 | 财经日报 |
| 世界杯预选赛:国足 2:1 击败对手,出线形势明朗 | 体育 | 体育周刊 |
| 热门剧集《星途》大结局收视率破纪录 | 娱乐 | 娱乐头条 |
| AI 芯片需求爆发,国产 GPU 厂商加速追赶 | 科技 | 科技前沿 |
| 央行释放流动性信号,市场预期降准临近 | 财经 | 财经日报 |
| NBA 季后赛:湖人队逆转晋级,詹姆斯砍下 40 分 | 体育 | 体育周刊 |
| 新综艺《声生不息》首播获好评,豆瓣评分 8.5 | 娱乐 | 娱乐头条 |
| 量子计算突破:中国团队实现 512 量子比特纠缠 | 科技 | 科技前沿 |
| 新能源汽车销量再创新高,渗透率突破 45% | 财经 | 财经日报 |
| 城市马拉松热潮:上半年全国举办超 200 场赛事 | 体育 | 体育周刊 |
| 经典电影《花样年华》4K 修复版即将重映 | 娱乐 | 娱乐头条 |
三、下拉刷新实现
3.1 Refresh 嵌套 List
Refresh({ refreshing: $$this.refreshing }) {
List() {
ForEach(this.getFilteredNews(), (item: NewsItem, idx: number) => {
ListItem() {
// 新闻卡片
}
}, (item: NewsItem) => item.id.toString())
}
.width('100%')
.layoutWeight(1)
.scrollBar(BarState.Off)
.edgeEffect(EdgeEffect.Spring)
}
.onRefreshing(() => { this.doRefresh(); })
layoutWeight(1) 让 List 占据剩余空间,edgeEffect(EdgeEffect.Spring) 提供回弹效果——当滚动到顶部继续下拉时,列表会产生弹性形变,这是触发 Refresh 的前奏。
3.2 刷新逻辑
doRefresh(): void {
this.refreshing = true;
setTimeout(() => {
this.refreshing = false;
promptAction.showToast({ message: '已刷新,暂无新内容', duration: 1500 });
}, 1200);
}
Demo 中用 1.2 秒的延迟模拟网络请求。在真实应用中,这里应当调用后端 API 获取最新数据,并在成功回调中设置 this.refreshing = false。Demo 总是返回"暂无新内容"——因为数据是硬编码的,但 Toast 提示让用户明确知道刷新操作已生效。
3.3 Refresh 与 List 的协作机制
这里有一个重要的架构细节:getFilteredNews() 方法每次调用时都会从 this.allNews 重新计算过滤结果。为什么不在 @State results 中维护一个独立的过滤数组?
答案在于 ArkUI 的 @State 响应机制。如果维护两个独立的 @State 数组(allNews 和 results),每次修改收藏或已读状态时,需要同时更新两个数组——这不仅增加代码复杂度,还容易出错。
使用"单一数据源 + 计算属性"模式:allNews 是唯一的真实数据,getFilteredNews() 是它的过滤视图。当用户切换分类或点击"仅看收藏"时,ArkUI 检测到 selectedCategory 或 showBookmarked 的变化,自动重新渲染列表——无需手动维护中间状态。
四、分类筛选
4.1 横向标签栏
分类标签以横向滚动的标签栏展示:
Scroll() {
Row() {
ForEach(this.getCategories(), (cat: string, idx: number) => {
Row() {
Text(cat)
if (this.getUnreadCount(cat) > 0) {
Text(this.getUnreadCount(cat).toString())
.backgroundColor(...)
}
}
.backgroundColor(cat === this.selectedCategory ? '#1677FF' : '#FFFFFF')
.onClick(() => { this.selectedCategory = cat; })
})
}
}
.scrollBar(BarState.Off)
选中分类使用蓝色填充 + 白色文字,未选中使用白色填充 + 灰色文字——视觉对比强烈,用户一眼即可识别当前位置。
每个标签旁边显示该分类的未读数量小徽标,让用户无需进入分类就能感知"这个分类里还有多少没看的"。这个徽标在选中和未选中状态下使用不同的颜色(白色半透明 vs 浅灰),保持视觉层次的一致性。
4.2 过滤器组合
分类筛选和收藏筛选可以叠加使用:
getFilteredNews(): NewsItem[] {
let news = this.allNews.slice();
if (this.selectedCategory !== '推荐') {
news = news.filter((item: NewsItem) => item.category === this.selectedCategory);
}
if (this.showBookmarked) {
news = news.filter((item: NewsItem) => item.bookmarked);
}
return news;
}
两个过滤器是"与"关系——先按分类缩小范围,再按收藏状态进一步过滤。用户可以在"科技"分类下仅看收藏文章,这在真实使用场景中非常有用(“我在科技分类下收藏了哪些文章?”)。
五、收藏与已读管理
5.1 收藏切换
每条新闻卡片右上角有一个星标图标,点击切换收藏状态:
Text(item.bookmarked ? '★' : '☆')
.fontSize(18)
.fontColor(item.bookmarked ? '#FAAD14' : '#CCCCDD')
.onClick(() => { this.toggleBookmark(item.id); })
已收藏显示金色实心星标(★),未收藏显示灰色空心星标(☆)。收藏操作是即时生效的——点击卡片上的星标时,stopPropagation 不会阻止事件冒泡。因此需要注意:点击星标会同时触发 toggleBookmark(星标的 onClick)和 markRead(卡片的 onClick)。这实际上是合理的行为——用户收藏一篇文章时,通常也已经"阅读"了它(至少浏览了标题)。
5.2 已读标记
点击新闻卡片将文章标记为已读:
.onClick(() => { this.markRead(item.id); })
已读状态通过标题的字重体现:未读文章标题使用 FontWeight.Bold(加粗),已读文章使用 FontWeight.Normal(常规)。同时每张卡片上方会显示一个红色的"NEW"标记表明未读状态。
5.3 全部已读
工具栏右侧提供"全部已读"按钮(仅在有未读文章时显示):
if (this.getUnreadCount() > 0) {
Text('全部已读')
.onClick(() => { this.markAllRead(); })
}
markAllRead 将数组中所有对象的 read 字段置为 true,然后显示 Toast 确认:
markAllRead(): void {
this.allNews = this.allNews.slice().map((item: NewsItem) => {
return item.withRead(true);
});
promptAction.showToast({ message: '已全部标为已读', duration: 1500 });
}
5.4 未读计数
标题栏右侧显示全局未读总数,分类标签上显示各分类未读数。两个计数都通过 getUnreadCount() 方法实时计算:
getUnreadCount(category?: string): number {
let news = this.allNews.slice();
if (category) {
news = news.filter((item: NewsItem) => item.category === category);
}
return news.filter((item: NewsItem) => !item.read).length;
}
不传参数时统计全部分类,传分类名时只统计该分类。随着用户浏览和标记已读,这些数字实时变化——这是 @State 响应式更新的直观体现。
六、页面状态管理
6.1 四种视图状态
页面存在四种可能的视图状态:
- 正常列表:有新闻数据,正常展示
- 空分类:某个分类下没有匹配的新闻
- 空收藏:开启了"仅看收藏"但无收藏文章
- 刷新中:下拉刷新加载指示器显示中
每种状态都有对应的 UI 反馈:
if (this.getFilteredNews().length === 0) {
ListItem() {
Column() {
Text(this.showBookmarked ? '暂无收藏文章' : '暂无新闻')
Text(this.showBookmarked ? '点击星标图标收藏文章' : '试试切换其他分类')
}
}
}
空状态不是空白页面——它明确告知用户当前状态并给出下一步建议。
6.2 收藏筛选切换
工具栏的"收藏"按钮是一个切换开关:
Row() {
Text(this.showBookmarked ? '全部' : '收藏')
.fontColor(this.showBookmarked ? '#1677FF' : '#888899')
}
.backgroundColor(this.showBookmarked ? '#F0F5FF' : '#F8F9FA')
.onClick(() => { this.showBookmarked = !this.showBookmarked; })
激活时按钮文字变为"全部"(点击返回全量视图),未激活时显示"收藏"(点击进入仅看收藏)。按钮的蓝色高亮状态明确告知用户当前处于筛选模式中。
6.3 工具栏
工具栏位于分类标签下方,提供三个信息/操作:
- 结果计数(左):“共 X 条新闻”
- 收藏切换(右):切换收藏筛选
- 全部已读(右):一键标记全部为已读
工具栏的布局使用了 Blank() 将左侧计数和右侧按钮组隔开,信息层级清晰。
七、视觉效果与交互细节
7.1 新闻卡片设计
每条新闻卡片包含三个层次:
- 元信息行(顶部):分类标签(蓝底) + 来源名称(灰字) + NEW 标记(红字) + 时间(灰字) + 收藏星标(右对齐)
- 标题行(中部):15px 加粗/常规,最多 2 行
- 摘要行(下部):13px 灰色,最多 2 行
三行内容形成清晰的信息层级:先看分类和来源判断可信度,再看标题了解核心内容,最后看摘要获取细节。卡片之间用 Divider 分隔,避免拥挤感。
7.2 首尾卡片的圆角处理
多篇新闻以"卡片组"的形式展示——第一项有顶部圆角,最后一项有底部圆角(借用 BorderRadius.LG),中间的卡片为直角。当只有一条结果时,四个角都是圆角:
.borderRadius(idx === 0 && this.getFilteredNews().length === 1 ? BorderRadius.LG :
(idx === 0 ? { topLeft: 12, topRight: 12, bottomLeft: 0, bottomRight: 0 } : {}))
7.3 分类标签的未读徽标
分类标签中的未读计数使用了视觉分层:
backgroundColor(cat === this.selectedCategory ? '#FFFFFF30' : '#F2F3F5')
选中时使用白色半透明(#FFFFFF30),与蓝色选中背景形成层次对比;未选中时使用淡灰(#F2F3F5),与白色背景保持微妙差异。这个细节让 5 个分类标签的视觉处理各异但统一。
八、交互流程演示
8.1 浏览推荐页
进入页面时,默认选中"推荐"分类,展示全部 12 篇新闻。标题栏显示未读数"12",每个分类标签旁有小徽标显示各类别未读数。所有标题加粗显示(未读状态),卡片上有红色 NEW 标记。
8.2 下拉刷新
在列表顶部继续下拉。当超过阈值时,刷新指示器出现。1.2 秒后 Toast 提示"已刷新,暂无新内容"。
8.3 分类切换
点击"科技"分类标签。列表从 12 篇变为 3 篇(仅科技类新闻)。工具栏显示"共 3 条新闻"。点击"财经"标签,列表切换到 3 篇财经新闻。
8.4 阅读与收藏
点击一篇科技新闻。该新闻的标题从加粗变为常规(已读),NEW 标记消失。标题栏未读数从 12 变为 11,"科技"分类标签的未读徽标同步更新。
点击该新闻的星标(☆),变为金色(★),表示已收藏。再点击"收藏"筛选按钮,列表仅显示这一篇文章。
8.5 全部已读
返回"推荐"分类。点击"全部已读"按钮,所有文章的标题变为常规字重,NEW 标记全部消失,标题栏未读数归零。Toast 提示"已全部标为已读"。
九、总结
本文通过"新闻资讯流"这个实战案例,全面讲解了 ArkUI Refresh 组件的使用方法。核心知识点包括:
- Refresh 组件:
$$双向绑定 +onRefreshing回调 +refreshing状态管理 - List + Refresh 协作:List 作为 Refresh 子组件,提供滚动容器和下拉检测
- 分类筛选:横向滚动标签栏 + 未读计数徽标 + 多过滤器组合
- 收藏/已读管理:class 工厂方法替代对象展开 +
slice().map()触发响应式更新 - 四种视图状态:正常 / 空分类 / 空收藏 / 刷新中,每种有对应的 UI 反馈
- 单一数据源模式:
allNews为唯一真实数据,getFilteredNews()为计算视图
下拉刷新是移动应用中最值得打磨的交互之一。一个好的下拉刷新不仅仅是"能拉下来转圈"——它需要在最恰当的时机给用户最舒适的反馈,同时与列表筛选、状态管理等周边功能无缝协作。ArkUI 的 Refresh 组件处理了手势识别和动画帧的底层细节,让开发者可以专注于刷新逻辑、数据结构和 UI 反馈的设计。
更多推荐



所有评论(0)