
文章目录前言代码讲解页面入口怎么安排数据模型先稳住状态变量控制页面反馈布局代码不要从样式开始读Builder 和方法承担复用使用方式完整代码关键代码解析interface 不是摆设State 是交互的开关列表和卡片要靠数据驱动事件回调要短一点样式参数别急着抽常量总结前言下拉刷新最容易被写成一个假效果。要像真实列表就要有刷新、追加、点赞这些状态变化。我建议把这篇当成一个可以直接改造的 ArkUI 页面模板来看。先跑起来确认页面展示和交互都正常再把里面的模拟数据替换成接口数据最后再调整颜色、间距和业务字段。这样改起来比较稳不容易一边改样式一边把状态逻辑弄乱。这篇文章会按「页面意图、数据结构、状态流转、布局组织、交互细节」这条线来读代码。你不用从第一行样式硬看到最后先抓住主线后面的细节会轻松很多。代码讲解页面入口怎么安排代码里的入口组件是RefreshFeedListPage。如果你把它当成单独页面预览保留Entry就可以如果要塞进现有工程的路由体系通常会去掉Entry再由外层页面或路由模块统一管理。这个习惯很重要。一个项目里入口页面太多预览时容易混后期拆组件也会麻烦。我的做法是案例阶段保留入口接业务时只保留真正需要被路由访问的页面。数据模型先稳住本案例涉及的数据模型主要有FeedPost。这些interface的作用不是为了“看起来规范”而是给 UI 层划边界。比如列表项需要标题、描述、颜色、选中态就应该在模型里说清楚。后续从接口拿数据时可以在请求层做字段映射别让后端字段名直接污染页面代码。如果你准备接真实业务建议把模拟数组替换成接口返回后的 ViewModel。页面只关心自己要渲染什么不关心接口原始字段长什么样。状态变量控制页面反馈本案例里的关键状态包括isRefreshing、refreshCount、feedList、loadingMore。State的职责是让 UI 对用户操作有反馈。比如选中某个卡片、切换分类、展开内容、修改输入值、控制加载中状态这些都应该由状态驱动。状态一变相关 UI 自动刷新这也是 ArkUI 声明式开发最核心的体验。这里有个容易踩的坑不要把所有数据都塞进State。静态配置用普通数组就够了只有用户会改、接口会更新、页面需要重新渲染的数据才适合放进状态里。布局代码不要从样式开始读这个页面里比较关键的组件有Row横向排列、左右分栏、按钮组或卡片行都依赖它来控制水平方向的节奏。Column纵向组织标题、内容、操作区适合搭页面主骨架。Divider负责信息分组不只是画一条线而是帮用户理解层级。List适合长列表和分组列表重点是数据过滤、滚动和状态同步。Grid适合固定宫格、桌面图标、内容卡片等二维排布。Refresh用于下拉刷新真正要讲清楚的是 refreshing 状态什么时候开始和结束。读布局时我更建议先找大容器再看内部如何拆块。比如一个页面通常会先用Column纵向放标题区、内容区、操作区内容区里再用Row、Grid、List或其他容器细分。这样看代码层级会比逐行看.padding()、.fontSize()清楚得多。如果页面显示错位优先检查容器关系而不是马上改像素。很多问题不是某个.margin()不对而是外层容器的宽高、滚动方向、权重分配或对齐方式没想清楚。Builder 和方法承担复用这篇没有额外拆出 Builder页面结构主要集中在build()中。辅助方法主要有aboutToAppear、doRefresh、toggleLike。Builder更适合复用 UI 片段比如卡片、列表行、头部区域、按钮组。普通方法更适合放计算逻辑比如筛选列表、统计数量、切换状态、生成颜色。两者分清楚后页面会干净很多。我不建议把所有东西都塞进build()。刚开始代码少还行需求一多build()会变成几百行后面改一个按钮都要找半天。使用方式把下面的完整代码放到 ArkTS 页面文件里即可运行。文章中的组件名已经是语义化命名不包含编号式案例名称接路由时直接使用RefreshFeedListPage就行。如果你要改成业务页面优先改三处数据模型、状态变量、事件方法。样式可以后调先保证数据流和交互是通的。完整代码// RefreshFeedListPage: 下拉刷新 - Refresh 组件 // 知识点: Refresh、refreshing、onRefreshing、onStateChange、自定义刷新动画 interface FeedPost { postId: number authorName: string authorEmoji: string authorBg: string postContent: string postTime: string likeCount: number commentCount: number shareCount: number isLiked: boolean tagLabel: string tagColor: string } Entry Component struct RefreshFeedListPage { State isRefreshing: boolean false State refreshCount: number 0 State feedList: FeedPost[] [] State loadingMore: boolean false private allPosts: FeedPost[] [ { postId: 1, authorName: 技术探索者, authorEmoji: , authorBg: #E3F2FD, postContent: 刚发现 HarmonyOS 5.0 的 Observed 和 ObjectLink 装饰器配合使用可以实现跨组件的深层属性响应太强了, postTime: 刚刚, likeCount: 42, commentCount: 8, shareCount: 3, isLiked: false, tagLabel: 鸿蒙, tagColor: #3B82F6 }, { postId: 2, authorName: 设计魔法师, authorEmoji: , authorBg: #FCE4EC, postContent: 分享一个配色方案主色 #007DFF 辅助色 #10B981 警告色 #F59E0B这套配色在科技类 App 里非常耐看。, postTime: 5分钟前, likeCount: 89, commentCount: 15, shareCount: 12, isLiked: true, tagLabel: 设计, tagColor: #EC4899 }, { postId: 3, authorName: 全栈小王子, authorEmoji: , authorBg: #FFF8E1, postContent: 今天用 Node.js ArkTS 搭了个全栈项目模板前后端类型共享开发体验拉满。代码已开源需要的自取 , postTime: 30分钟前, likeCount: 156, commentCount: 32, shareCount: 28, isLiked: false, tagLabel: 开源, tagColor: #10B981 }, { postId: 4, authorName: 极客生活家, authorEmoji: , authorBg: #F3E5F5, postContent: 静电容键盘 竖屏显示器 站立桌 我的最终工作站形态腰再也不疼了, postTime: 1小时前, likeCount: 203, commentCount: 45, shareCount: 18, isLiked: false, tagLabel: 数码, tagColor: #8B5CF6 }, { postId: 5, authorName: 代码诗人, authorEmoji: ✍️, authorBg: #E0F7FA, postContent: 写代码这件事就像写诗——第一次写出来的版本往往是最差的不断重构才能找到最优雅的表达方式。, postTime: 2小时前, likeCount: 78, commentCount: 12, shareCount: 9, isLiked: true, tagLabel: 感悟, tagColor: #6366F1 }, { postId: 6, authorName: 效率达人, authorEmoji: ⚡, authorBg: #E8F5E9, postContent: 分享我的时间管理方法番茄工作法25分钟专注 5分钟休息已经坚持了3个月效率提升了40%以上。, postTime: 3小时前, likeCount: 125, commentCount: 23, shareCount: 35, isLiked: false, tagLabel: 效率, tagColor: #F59E0B }, ] private extraPosts: FeedPost[] [ { postId: 7, authorName: 产品思考者, authorEmoji: , authorBg: #FFF3E0, postContent: 做产品最难的不是想到好创意而是知道什么时候应该删除一个功能。, postTime: 4小时前, likeCount: 67, commentCount: 19, shareCount: 7, isLiked: false, tagLabel: 产品, tagColor: #F97316 }, { postId: 8, authorName: 前端炼金术, authorEmoji: ⚗️, authorBg: #E8EAF6, postContent: CSS Grid 布局在处理复杂布局时真的是神器配合 minmax() 和 auto-fill响应式布局只需几行代码。, postTime: 5小时前, likeCount: 93, commentCount: 17, shareCount: 22, isLiked: false, tagLabel: 前端, tagColor: #3B82F6 }, ] aboutToAppear() { this.feedList this.allPosts.slice() } doRefresh() { setTimeout(() { this.refreshCount // 模拟刷新后在顶部插入新数据 const newPost: FeedPost { postId: 100 this.refreshCount, authorName: 刷新获取, authorEmoji: ✨, authorBg: #FFF0F6, postContent: 【第 ${this.refreshCount} 次刷新获取的新内容】HarmonyOS 开发小技巧合理使用 Builder 装饰器可以有效减少组件树的层级提升渲染性能。, postTime: 刚刚, likeCount: 0, commentCount: 0, shareCount: 0, isLiked: false, tagLabel: 新内容, tagColor: #EC4899 } this.feedList [newPost].concat(this.feedList) this.isRefreshing false }, 1500) } toggleLike(postId: number) { for (let i 0; i this.feedList.length; i) { if (this.feedList[i].postId postId) { this.feedList[i].isLiked !this.feedList[i].isLiked this.feedList[i].likeCount this.feedList[i].isLiked ? this.feedList[i].likeCount 1 : this.feedList[i].likeCount - 1 break } } } build() { Column({ space: 0 }) { // 顶部标题 Row({ space: 0 }) { Text( 动态) .fontSize(20) .fontWeight(FontWeight.Bold) .fontColor(#1A1A1A) .layoutWeight(1) if (this.refreshCount 0) { Text(已刷新 ${this.refreshCount} 次) .fontSize(12) .fontColor(#888888) .margin({ right: 8 }) } Text(✏️) .fontSize(20) } .width(100%) .height(52) .padding({ left: 16, right: 16 }) .backgroundColor(#FFFFFF) .border({ width: { bottom: 1 }, color: #F0F0F0 }) // Refresh 下拉刷新 Refresh({ refreshing: $$this.isRefreshing }) { List({ space: 0 }) { ForEach(this.feedList, (post: FeedPost) { ListItem() { Column({ space: 0 }) { Row({ space: 12 }) { // 头像 Text(post.authorEmoji) .fontSize(26) .width(46) .height(46) .textAlign(TextAlign.Center) .backgroundColor(post.authorBg) .borderRadius(23) .flexShrink(0) Column({ space: 4 }) { Row({ space: 8 }) { Text(post.authorName) .fontSize(14) .fontWeight(FontWeight.Bold) .fontColor(#1A1A1A) .layoutWeight(1) Text(post.tagLabel) .fontSize(11) .fontColor(post.tagColor) .padding({ left: 6, right: 6, top: 2, bottom: 2 }) .backgroundColor(${post.tagColor}18) .borderRadius(4) } .width(100%) Text(post.postTime) .fontSize(12) .fontColor(#BBBBBB) } .layoutWeight(1) .alignItems(HorizontalAlign.Start) } .width(100%) .alignItems(VerticalAlign.Center) .margin({ bottom: 10 }) // 正文 Text(post.postContent) .fontSize(14) .fontColor(#333333) .lineHeight(22) .width(100%) // 操作栏 Row({ space: 0 }) { Row({ space: 4 }) { Text(post.isLiked ? ❤️ : ) .fontSize(16) Text(post.likeCount.toString()) .fontSize(13) .fontColor(post.isLiked ? #EF4444 : #999999) } .layoutWeight(1) .onClick(() { this.toggleLike(post.postId) }) Row({ space: 4 }) { Text() .fontSize(16) Text(post.commentCount.toString()) .fontSize(13) .fontColor(#999999) } .layoutWeight(1) Row({ space: 4 }) { Text(↗️) .fontSize(16) Text(post.shareCount.toString()) .fontSize(13) .fontColor(#999999) } .layoutWeight(1) } .width(100%) .margin({ top: 10 }) } .padding({ left: 16, right: 16, top: 14, bottom: 14 }) .backgroundColor(#FFFFFF) } }, (post: FeedPost) post.postId.toString()) // 底部加载更多 ListItem() { Column({ space: 0 }) { Divider().strokeWidth(6).color(#F5F6FA) Text(this.loadingMore ? 正在加载更多... : ⬇️ 上滑加载更多) .fontSize(13) .fontColor(#BBBBBB) .width(100%) .textAlign(TextAlign.Center) .height(48) .onClick(() { if (!this.loadingMore) { this.loadingMore true setTimeout(() { this.feedList this.feedList.concat(this.extraPosts) this.loadingMore false }, 1000) } }) } } } .divider({ strokeWidth: 6, color: #F5F6FA }) .scrollBar(BarState.Off) .edgeEffect(EdgeEffect.None) } .onRefreshing(() { this.doRefresh() }) .onStateChange((state: RefreshStatus) { if (state RefreshStatus.Drag) { // 下拉中 } }) .layoutWeight(1) } .width(100%) .height(100%) .backgroundColor(#F5F6FA) } }关键代码解析interface不是摆设很多新手写 ArkUI 页面时会直接在ForEach里使用一堆临时对象。页面小的时候没问题但字段一多就很难知道每个字段到底用于哪里。本案例把数据结构提前声明出来读代码时能很快判断每个 UI 区块依赖哪些字段。后续如果接接口也建议保留这层结构。接口可以变页面模型尽量稳定。这样页面不会因为后端多返回一个字段、少返回一个字段就跟着大改。State是交互的开关本案例的交互反馈都围绕状态展开。点击、切换、输入、刷新这类行为本质上都是修改某个状态值然后让 UI 重新计算显示结果。写这类代码时有一个简单判断如果某个值变化后页面应该立刻变那它大概率应该是状态如果它只是配置项、静态文案、固定颜色就不要放进状态。列表和卡片要靠数据驱动只要页面里出现重复结构就应该优先想到数据数组加ForEach。这样新增一项、删除一项、调整顺序都只动数据不用复制粘贴一段 UI。这个案例的完整代码里已经把主要内容抽成数组或方法。你可以试着多加一条数据看页面是否能自然渲染出来。如果能说明结构是健康的如果加一条就要改很多 UI说明组件拆分还不够。事件回调要短一点.onClick()、onChange()这类回调里可以直接改状态但不建议堆太多逻辑。简单切换可以写在回调里复杂逻辑最好拆成方法。这样做有两个好处一是读页面结构时不会被业务逻辑打断二是后面要加埋点、请求接口、异常处理时有明确的位置可以改。样式参数别急着抽常量这个案例里有不少颜色、间距、圆角、字号。学习阶段直接写在组件链上反而更容易看出效果。等你确定页面风格稳定后再把主题色、通用间距、卡片圆角提到统一常量里。过早抽象会让示例代码变绕。先让页面清楚再谈工程化这是我比较推荐的顺序。总结这个案例真正值得学的不只是某个 ArkUI 组件怎么写而是一个页面如何从数据走到状态再走到布局和交互。你可以按这个顺序改造先替换数据模型再确认状态更新是否正确接着调整布局容器最后打磨样式细节。只要这条主线不乱页面规模变大也不会失控。如果后续继续扩展我会优先把可复用的卡片、列表行、筛选栏或控制区拆成独立组件。这样主页面只保留数据和流程代码会更接近真实项目里的写法。