HarmonyOS ArkTS 列表渲染与数据持久化:从历史记录页面看 List 组件与存储方案选型
引子列表是应用的记忆一个完整的应用还需要记住用户做过什么。骰子应用的历史记录页面就承担了这个职责——它把每次投掷的结果保存下来让用户可以回顾。完整效果HistoryTab 的代码结构先看历史记录页面的核心代码BuilderHistoryTab(){Column(){Row(){Text(历史).fontSize(FontSize.title).fontColor(this.gc().text).fontWeight(FontWeight.Bold)Blank()if(this.history.length0){Text(清空).fontSize(FontSize.sm).fontColor(#EF4444).onClick(()this.clr())}}.width(100%).padding({left:Spacing.md,right:Spacing.md,top:Spacing.lg,bottom:Spacing.sm})if(this.history.length0){Column(){Text().fontSize(48)Text(还没有投掷记录).fontSize(FontSize.md).fontColor(this.gc().text2)}.width(100%).layoutWeight(1).justifyContent(FlexAlign.Center)}else{List(){ForEach(this.history,(r:RollResult){ListItem(){Row(){Text(DICE_TYPES.find((d:DiceType)d.keyr.diceType)?.emoji||).fontSize(22)Column(){Text((DICE_TYPES.find((d:DiceType)d.keyr.diceType)?.label||) ×String(r.results.length)).fontSize(FontSize.sm).fontColor(this.gc().text)Text([r.results.join(, )]).fontSize(FontSize.xs).fontColor(this.gc().text2).maxLines(1)}.margin({left:Spacing.sm}).alignItems(HorizontalAlign.Start)Blank()Column(){Text(String(r.total)).fontSize(FontSize.lg).fontColor(this.gc().accent).fontWeight(FontWeight.Bold)Text(this.fmt(r.rolledAt)).fontSize(FontSize.xs).fontColor(this.gc().text2)}}.width(100%).padding(Spacing.md).backgroundColor(this.gc().surface).borderRadius(BorderRadius.md).margin({bottom:Spacing.sm,left:Spacing.md,right:Spacing.md})}})}.width(100%).layoutWeight(1).scrollBar(BarState.Off)}}.width(100%).height(100%)}这段代码虽然不长但包含了几个重要的设计模式空状态处理、列表渲染、数据格式化、批量操作。空状态设计用户体验的细节当历史记录为空时页面显示一个友好的提示if(this.history.length0){Column(){Text().fontSize(48)Text(还没有投掷记录).fontSize(FontSize.md).fontColor(this.gc().text2)}.width(100%).layoutWeight(1).justifyContent(FlexAlign.Center)}空状态设计是用户体验的重要细节。很多开发者觉得空就空呗有什么好显示的但其实空状态有三个作用解释现状告诉用户这里没有数据而不是让用户猜测是不是出了 bug引导操作暗示用户去投掷几次骰子吧视觉平衡避免页面大面积空白保持视觉完整性这个应用的空状态设计还不错但可以更进一步——加一个按钮引导用户去投掷if(this.history.length0){Column(){Text().fontSize(48)Text(还没有投掷记录).fontSize(FontSize.md).fontColor(this.gc().text2).margin({top:Spacing.sm})Button(去投掷骰子).fontSize(FontSize.md).fontColor(#FFFFFF).backgroundColor(this.gc().primary).borderRadius(BorderRadius.xl).margin({top:Spacing.md}).onClick(()this.tabdice)// 切换到骰子 Tab}.width(100%).layoutWeight(1).justifyContent(FlexAlign.Center)}这样用户看到空状态后可以一键跳转到骰子页面而不是手动点击底部 Tab。列表渲染ForEach vs List代码中用了ListForEach的组合List(){ForEach(this.history,(r:RollResult){ListItem(){...}})}.width(100%).layoutWeight(1).scrollBar(BarState.Off)这里有两个关键点1. 为什么用 List 而不是 Column如果用ColumnForEach所有列表项都会被渲染即使用户只看前几项。当数据量大时这会导致初始渲染慢需要创建所有列表项的 UI 元素内存占用高所有列表项都占用内存滚动卡顿滚动时需要重新布局所有元素List组件支持懒加载只渲染用户可见的列表项。当用户滚动时List会自动创建新的列表项销毁不可见的列表项。这样无论数据量多大渲染的列表项数量都是固定的大约 10-20 个性能稳定。2. ForEach 的 key 参数ForEach的第二个参数是 item generator第三个参数是 key generator可选。代码中没有提供 key这在数据量小时没问题但当数据量大时可能导致渲染问题。比如用户删除了一条记录ForEach会重新渲染整个列表。如果没有稳定的 keyArkUI 可能会复用错误的列表项导致显示异常。建议添加 keyForEach(this.history,(r:RollResult,index:number){ListItem(){...}},(r:RollResult,index:number)index.toString())用索引作为 key 虽然不是最优解理想情况应该用唯一 ID但比没有 key 好。数据格式化时间戳的处理代码中用fmt方法格式化时间戳privatefmt(ts:number):string{constdnewDate(ts);return(d.getMonth()1)/d.getDate() String(d.getHours()).padStart(2,0):String(d.getMinutes()).padStart(2,0);}这个方法把时间戳格式化为M/D HH:MM的格式比如7/17 19:35。为什么不用 toLocaleStringJavaScript 和 ArkTS 都提供了toLocaleString()方法可以自动根据用户 locale 格式化日期。但代码没有用它原因是一致性toLocaleString()的输出格式取决于系统设置不同设备可能不一样可控性手动格式化可以精确控制输出格式性能toLocaleString()涉及 locale 计算性能略差对于这个应用手动格式化是合理的选择。但要注意getMonth()返回的月份是从 0 开始的0-11所以需要加 1。更好的时间格式化方案手动拼接字符串容易出错建议用工具函数functionformatTime(timestamp:number):string{constdnewDate(timestamp);constmonthd.getMonth()1;constdayd.getDate();consthoursString(d.getHours()).padStart(2,0);constminutesString(d.getMinutes()).padStart(2,0);return${month}/${day}${hours}:${minutes};}这样代码更清晰也更容易维护。如果将来要改格式比如加年份只需修改这一个函数。数据查找DICE_TYPES.find() 的性能代码中多次用DICE_TYPES.find()来查找骰子类型Text(DICE_TYPES.find((d:DiceType)d.keyr.diceType)?.emoji||)find()方法会遍历数组直到找到匹配的元素。对于 7 个元素的DICE_TYPES数组性能完全没问题。但如果数组很大比如几百个元素频繁调用find()可能有性能问题。优化方案是用 MapconstDICE_MAPnewMapstring,DiceType();DICE_TYPES.forEach((d:DiceType)DICE_MAP.set(d.key,d));// 使用Text(DICE_MAP.get(r.diceType)?.emoji||)Map 的查找是 O(1) 复杂度比find()的 O(n) 快很多。但在这个应用中性能差异可以忽略不计用find()就够了。数据持久化DiceDatabase 的设计DiceDatabase类封装了底层的存储逻辑。虽然看不到完整代码但从使用方式可以推断出它的接口设计classDiceDatabase{asyncinit():Promisevoid{...}add(result:RollResult):void{...}getHistory():RollResult[]{...}asyncclear():Promisevoid{...}}Preferences API vs 关系型数据库对于这种简单的键值对存储HarmonyOS 的 Preferences API 就够了。Preferences 适合存储少量配置数据API 简单性能好。但如果要支持复杂查询比如按日期筛选、统计分析建议用关系型数据库 RDB。特性Preferences关系型数据库 (RDB)数据结构键值对表、行、列查询能力按 key 获取SQL 查询性能读写快复杂查询慢适用场景配置、少量数据大量结构化数据数据上限约 1MB无硬性限制在这个应用中历史记录可能会增长到几百条。如果用 Preferences每次获取历史记录都需要读取整个数据集性能会下降。而 RDB 支持分页查询可以只获取最近的 N 条记录。数据同步策略代码中每次投掷后都会调用this.db.getHistory()重新获取历史列表if(this.db){this.db.add(createRollResult(this.diceType,fr));this.historythis.db.getHistory();}这种写后读的策略简单可靠但不是最优的。更好的方案是if(this.db){constnewResultcreateRollResult(this.diceType,fr);this.db.add(newResult);this.history[newResult,...this.history];// 直接更新状态}这样不需要重新从数据库读取直接把新结果添加到列表开头。但要注意这种方式假设add()一定成功。如果add()失败状态和数据库就不一致了。需要权衡一致性和性能。数据清空的确认代码中点击清空按钮会直接清空所有历史记录if(this.history.length0){Text(清空).fontSize(FontSize.sm).fontColor(#EF4444).onClick(()this.clr())}这很危险——用户可能误触。建议添加确认弹窗privateshowClearConfirm:booleanfalse;// 在 UI 中AlertDialog({title:确认清空,message:确定要清空所有历史记录吗此操作不可恢复。,primaryButton:{value:取消,action:(){this.showClearConfirmfalse;}},secondaryButton:{value:清空,fontColor:#EF4444,action:(){this.clr();this.showClearConfirmfalse;}}})这样用户误触时还有机会取消避免数据丢失。列表项的布局分析每个历史记录项的布局是这样的┌─────────────────────────────────┐ │ 六面骰 ×2 14 7/17│ │ [3, 11] 19:35│ └─────────────────────────────────┘左边是骰子图标和类型信息右边是总和和时间。这种左信息、右数值的布局在移动端很常见适合展示概要信息。代码用RowBlank()来实现左右分布Row(){// 左侧图标和类型信息Text(emoji).fontSize(22)Column(){...}Blank()// 占据中间空间// 右侧总和和时间Column(){...}}Blank()是 ArkUI 的弹性空白组件会占据剩余空间。这种布局方式比手动计算宽度更灵活也更容易适配不同屏幕尺寸。列表项的点击交互当前代码没有给列表项添加点击事件。如果要查看详情或删除单条记录需要添加onClickRow(){...}.width(100%).padding(Spacing.md).backgroundColor(this.gc().surface).borderRadius(BorderRadius.md).onClick(()this.showDetail(r))// 添加点击事件但要注意列表项的点击事件和List组件的滚动事件可能会冲突。如果点击响应不灵敏可能需要调整点击区域或增加长按事件。性能优化大数据量的处理当历史记录增长到几百甚至上千条时性能优化就变得重要了。几个优化方向1. 分页加载不要一次性加载所有历史记录而是分页加载Statepage:number1;StatepageSize:number20;StateallHistory:RollResult[][];privateloadMore():void{conststart(this.page-1)*this.pageSize;constendstartthis.pageSize;constnewItemsthis.db.getHistoryRange(start,end);this.allHistory[...this.allHistory,...newItems];this.page;}在List组件的onReachEnd事件中触发加载更多List(){ForEach(this.allHistory,...)}.onReachEnd(()this.loadMore())2. 虚拟列表ArkUI 的List组件默认支持虚拟列表只渲染可见区域的元素。但需要确保ListItem的高度是固定的或可预测的否则虚拟列表的效果会打折扣。3. 图片和 Emoji 的渲染代码中用了 Emoji 作为图标、 等。Emoji 的渲染比图片快但有些设备可能不支持某些 Emoji。如果要兼容更多设备建议用图片资源代替 Emoji。总结历史记录页面虽然功能简单但涉及了 ArkUI 开发的多个核心知识点列表渲染、空状态设计、数据格式化、数据持久化、性能优化。在实际开发中这些知识点会反复出现只是复杂度不同。适用边界这个页面适合用作 ArkUI 列表渲染和数据持久化的入门案例涵盖了 List 组件、ForEach 渲染、空状态设计、时间格式化等核心知识点。但如果要上架应用商店还需要补充分页加载、删除确认、详情查看、数据导出等功能。建议在此基础上逐步扩展而不是一次性做完所有功能。对于 ArkTS 新手建议从类似的小项目入手逐步理解框架的设计哲学。ArkTS 的声明式 UI 和 React 有相似之处但状态管理和生命周期有明显区别需要花时间适应。对于有经验的开发者重点是理解 ArkTS 的约束——它不是 TypeScript 的简单扩展而是一个有自己规则的框架。遵循框架的最佳实践才能写出可维护、高性能的代码。

相关新闻

CANN/Ascend C获取存储带宽API

CANN/Ascend C获取存储带宽API

GetCoreMemBw 【免费下载链接】asc-devkit 本项目是CANN 推出的昇腾AI处理器专用的算子程序开发语言,原生支持C和C标准规范,主要由类库和语言扩展层构成,提供多层级API,满足多维场景算子开发诉求。 项目地址: https://gitcode.c…

2026/10/12 18:06:34 阅读更多 →
InstColorization架构详解:理解融合模块与实例分支的设计原理

InstColorization架构详解:理解融合模块与实例分支的设计原理

InstColorization架构详解:理解融合模块与实例分支的设计原理 【免费下载链接】InstColorization 项目地址: https://gitcode.com/gh_mirrors/in/InstColorization InstColorization是一个基于深度学习的实例感知图像着色系统,它通过创新的融合模…

2026/10/13 5:36:02 阅读更多 →
零基础构建知识图谱:3步实现非结构化数据到Neo4j的智能转换

零基础构建知识图谱:3步实现非结构化数据到Neo4j的智能转换

零基础构建知识图谱:3步实现非结构化数据到Neo4j的智能转换 【免费下载链接】llm-graph-builder Neo4j graph construction from unstructured data using LLMs 项目地址: https://gitcode.com/GitHub_Trending/ll/llm-graph-builder 想要将海量文档、网页和…

2026/9/29 9:01:18 阅读更多 →

最新新闻

CTF RSA常见题型与攻击脚本系统整理:从模数分解到共模攻击

CTF RSA常见题型与攻击脚本系统整理:从模数分解到共模攻击

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 5:35:33 阅读更多 →
Layui后台管理系统实战:从项目搭建到组件避坑指南

Layui后台管理系统实战:从项目搭建到组件避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 5:35:33 阅读更多 →
Kafka自定义分区器实战:解决负载倾斜与消息有序性

Kafka自定义分区器实战:解决负载倾斜与消息有序性

1. 为什么“自定义分区”不是锦上添花,而是Kafka生产链路的命门你有没有遇到过这样的场景:某天凌晨三点,监控告警疯狂刷屏——订单Topic的消费延迟突然飙升到2小时,下游风控服务集体超时。运维同学第一时间杀掉所有消费者重拉&…

2026/10/13 5:35:33 阅读更多 →
Agent技能库设计指南:从工具调用到可复用技能包

Agent技能库设计指南:从工具调用到可复用技能包

从 agent-skills 这个项目命名就能看出来,它要解决的是 Agent 技能沉淀的问题。我一开始做聊天机器人,总是往系统提示词里塞各种“你是个会查天气的助手”“你是个会算数的助手”这类话,结果上下文越写越长,功能越加越乱。后来我把…

2026/10/13 5:35:33 阅读更多 →
算子与运行时:AI系统性能的底层双引擎

算子与运行时:AI系统性能的底层双引擎

1. 这不是“概念课”,是写代码时每天都要面对的底层真相“第05章-算子与运行时”——看到这个标题,很多人第一反应是:教材目录?课程PPT?或者某本厚得让人想合上的系统编程书里的一页?但如果你最近在调试一个…

2026/10/13 5:35:33 阅读更多 →
手写Tool Use面试题:从函数到生产级工具调度系统

手写Tool Use面试题:从函数到生产级工具调度系统

1. 面试现场的真实压力:当“手写 Tool Use”成为临场能力试金石“请手写一个 Tool Use 实现。”这句话从面试官嘴里说出来的时候,我正端着一次性纸杯喝第三口温水——不是因为紧张,而是前两轮算法题刚结束,嗓子发干。但这句话一落…

2026/10/13 5:34:33 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →