HarmonyOS实战:用ArkTS构建复式统计表班级调查员

发布时间:2026/9/19 6:21:26
HarmonyOS实战:用ArkTS构建复式统计表班级调查员 第53个实例我选了《复式统计表班级调查员》。这段代码刚做完时我一度觉得它太简单无非是几个按钮、几个输入框、一张二维数组算出来的表。可等我真把统计数据、总人数、男女分别合计都放进去让班级里学生挨个点完一轮后才发现这个案例的扩展空间比看起来大得多。在HarmonyOS应用里做“班级调查员”本质上是让一个班级现场做数据采集每人选择性别和一项最喜欢的活动程序把每条记录落进二维矩阵最后自动生成一张复式统计表。它看起来像是一个“教学演示”但实际跑起来后你会发现它同时练到了 ArkTS 数据类型设计、State 状态管理、ForEach 列表渲染、Grid 表格布局、本地持久化这几块硬功夫非常适合刚入坑 HarmonyOS 声明式开发的人拿来当第二个完整项目练手。这个实例适合两类人。一类是已经会写 Button、TextInput、List但还没有独立做过“数据从录入到展示全流程”的开发者另一类是想把数学课“统计与概率”单元做成数字化活动的老师。下面我把设计思路、核心代码、踩坑记录和扩展建议完整写出来。1. 项目思路拆解统计不是背公式而是做调查1.1 复式统计表为什么适合做成App复式统计表是小学数学里一个经典知识点。单式统计表只统计一个维度比如“全班同学最喜欢去哪个场馆”而复式统计表至少包含两个维度行是活动项目列是性别分组中间的数字表示人数。它真正厉害的地方在于一张表能同时回答两个问题哪个活动最受欢迎男生和女生的选择有没有差异传统课堂上老师会让学生先举手、再数人数、最后把数字填进表里。这个过程问题很多点错一次就得重数男生女生容易混合计对不上更是家常便饭。把这个流程搬进 HarmonyOS App 后每次点击都会直接更新单元格永远不会出现“数完人数又忘了谁举手”的情况。所以这个应用的核心不是“画一张表”而是“让填表过程可重复、可纠错、可保存”。它给学生提供的是动手做统计的体验给老师提供的是一份自动汇总的课堂数据。理解了这一点功能范围就不会做歪不要一上来就加一堆花哨动画先把“录入一条记录、表格自动更新、数据能存下来”这三件事做扎实。1.2 功能范围与版本规划我建议分三个版本迭代不要试图一次做完。V1 只做数据录入和复式统计表展示V2 加本地保存和清空功能V3 加导出和简单的条形图/扇形图。V1 的具体功能是选择性别、选择活动项目、输入姓名点击提交后复式统计表对应单元格加一同时更新行合计、列合计和总人数。这个版本已经能完成一节课的调查任务。V2 要用 Preferences 把记录存到本地这样学生关闭应用再打开上一节课的数据还在。V3 把统计结果拼成文本导出顺便用 Canvas 画一个简单的横向条形图方便在黑板一体机上投屏讲解。我当时把自己限制在 V1没有直接写图表原因是表格和数据流不稳的时候图表只会放大错误。数据源头错一位画出来的图再好看也是误导。这个取舍很重要。2. 开发生态与基础准备2.1 开发工具与SDK版本这个实例是在 DevEco Studio 里新建的 Empty Ability 工程语言选 ArkTSAPI 版本用 12 或以上都行。工程建好后界面由 EntryAbility 加载的 Index 页面承载不涉及多模块、不涉及路由跳转结构非常干净。我建议新手不要一上来就用复杂模板。HarmonyOS 的工程模板里默认生成了很多抽象层对学习不一定是好事。新建一个精简工程把 main_pages.json 里只留一个 Index 页面所有代码集中在一两个文件里反而更容易建立“页面 状态 布局 交互”的认知。2.2 UI结构设计表单、按钮还是卡片第一版界面我采用的不是传统表单而是一组“选择按钮 输入框 提交按钮”。性别用两个按钮活动项目用四个按钮选中时按钮变色。这样设计的原因很直接班级环境下学生手指点击比键盘打字快而且点错能立即看到状态减少误操作。整个页面从上到下分四块区域标题区、录入区、统计表区、记录列表区。录入区放姓名输入框、性别选择、活动选择和提交按钮统计表区用 Grid 自动生成记录列表区则用 List 展示已经提交的调查记录方便老师核对谁还没填。这里有个小技巧录入区和统计表区不要挤在同一屏。如果空间不够统计表要保证完整宽度录入区可以通过滚动实现。否则学生点选项的时候很容易误触表格体验会很差。2.3 数据结构设计二维数组还是对象数组复式统计表的数据直观上就是一个二维矩阵行是活动项目列是性别。在这个项目里我同时用了“二维计数矩阵”和“调查记录对象数组”两份数据。调查记录对象数组负责保存每一次提交的原始信息比如姓名、性别、活动项目。二维计数矩阵负责渲染复式统计表按“行是场馆列是性别”的方式存储人数。为什么不用一份数据硬算因为统计表需要频繁改动单元格矩阵在 UI 渲染上更直接而原始记录需要支持后续排序、筛选、按姓名查重数组更方便。我觉得这两个职责分开代码会清晰得多。你需要记住一条原则不要为了省变量名把“原始数据”和“计算展示数据”混成一个结构。3. 核心模块实现从调查录入到表格生成3.1 数据模型与状态管理先定义调查记录的数据结构。ArkTS 里我会用类或者 interface里面至少要有 id、姓名、性别、活动项目四个字段。export class SurveyRecord { id: string; studentName: string; gender: string; activity: string; constructor(studentName: string, gender: string, activity: string) { this.id Date.now().toString() Math.floor(Math.random() * 10000); this.studentName studentName; this.gender gender; this.activity activity; } }id 字段很重要后面 List 渲染时要用它当 ForEach 的 key。不用 id 而用数组下标一旦删除中间某条记录UI 复用就会出现错乱。这种坑我踩过不止一次所以在结构设计阶段就加上 id 能省很多事。计数矩阵我用一个二维数组表示性别两行活动四列。为了让状态更新能被页面感知数组必须放在 State 里。State activityList: string[] [科技馆, 图书馆, 植物园, 少年宫]; State genderList: string[] [男生, 女生]; State countMatrix: number[][] [ [0, 0, 0, 0], [0, 0, 0, 0] ]; State records: SurveyRecord[] [];这里有一个绝大多数新手都会踩的坑你对countMatrix[0][1]做自增页面不会刷新。因为 ArkUI 的状态管理对嵌套数组的修改做不到像一维数组那样自动感知。我的做法是提交记录时重新创建一个新二维数组然后整体赋给countMatrix。private addRecord() { if (this.nameInput.trim().length 0) { this.showToast(请先输入姓名); return; } let newMatrix: number[][] []; for (let i 0; i this.countMatrix.length; i) { newMatrix.push([...this.countMatrix[i]]); } newMatrix[this.genderIndex][this.activityIndex] 1; this.countMatrix newMatrix; let record new SurveyRecord( this.nameInput.trim(), this.genderList[this.genderIndex], this.activityList[this.activityIndex] ); this.records.push(record); this.nameInput ; }你可能会问为什么非要深拷贝一份因为直接this.countMatrix[0][0] 1虽然数组内部数据变了但整个数组对象的引用没有变化状态管理器认为“数据没变”于是不触发重新渲染。整体赋值一个新数组引用变了UI 才会更新。这个理解比记住代码更值钱以后所有二维数据状态更新都可以照这个思路处理。3.2 班级调查员页面的输入交互录入区的交互我用TextInput输入姓名用Column Row Button做单选。按钮选中状态不靠 CSS class而是靠“选中下标是否等于当前按钮下标”来判断。Row({ space: 12 }) { ForEach(this.genderList, (gender: string, index: number) { Button(gender) .backgroundColor(this.genderIndex index ? #007DFF : #E5E5E5) .fontColor(this.genderIndex index ? #FFFFFF : #333333) .onClick(() { this.genderIndex index; }); }) }这里我把性别和活动项目都单独维护了一个选中下标genderIndex和activityIndex。刚开始学的时候你会觉得维护下标很麻烦不如存一个选中文本字符串。但存下标有实际好处计数矩阵的下标可以直接用不用每次通过findIndex把字符串转换成数字。在班级连续提交 30 个人的数据时这种细节能让新增记录的函数干净很多。活动选择的代码和性别选择一模一样只是把列表换成activityList下标换成activityIndex。为了不重复代码也可以抽一个通用组件。我第一版没抽因为只有两处重复抽组件反而要多传一堆参数。你要学会判断“重复两次要不要抽离”的分寸规则很简单三处以上再抽两处先保留。3.3 自动统计与复式表渲染统计表用 Grid 实现列模板固定为“第一列是活动项目后面每列是一个性别列”。Grid 的好处是列宽能用columnsTemplate统一控制不会出现表头和单元格对不齐的问题。列模板写法如下Grid() { GridItem() { Text(项目).fontSize(16).fontWeight(FontWeight.Bold) } ForEach(this.genderList, (gender: string) { GridItem() { Text(gender).fontSize(16).fontWeight(FontWeight.Bold) } }) ForEach(this.activityList, (activity: string, rowIndex: number) { GridItem() { Text(activity) } ForEach(this.genderList, (gender: string, colIndex: number) { GridItem() { Text(this.countMatrix[colIndex][rowIndex].toString()) } }) }) GridItem() { Text(合计) } ForEach(this.genderList, (gender: string, colIndex: number) { GridItem() { Text(this.getGenderTotal(colIndex).toString()) } }) } .columnsTemplate(1fr 1fr 1fr)因为activityList有四项、genderList有两组所以第一列加性别列一共三列columnsTemplate里就有三个1fr。每一行通过两个ForEach嵌套生成先输出活动项目再输出这一行对应的所有性别数量的单元格。合计行我用一个独立函数计算private getGenderTotal(colIndex: number): number { let total 0; for (let i 0; i this.activityList.length; i) { total this.countMatrix[colIndex][i]; } return total; }活动项目的行合计同理遍历性别这一列即可。最终做出来的表和数学课本上的复式统计表结构一致项目列在最左男生、女生分列最后一行列合计。我在实际测试时发现表格最大的坑不是数据算错而是“表头对齐”。如果用 Row 手动拼表头很容易因为字号、内边距不一致导致错位。换成 Grid 后所有单元格宽度由列模板统一分配这个问题就消失了。4. 持久化与导出把调查结果留下来4.1 Preferences 本地存储一节课调查完老师最怕的就是下课锁屏后数据全没了。所以 V2 我把记录通过 Preferences 写入本地。Preferences 是 HarmonyOS 提供的轻量级键值对存储适合存这种几十条的小数据量。写入时要把对象数组序列化成 JSON 字符串import { preferences } from kit.ArkData; const prefName survey_store; let pref: preferences.Preferences | null null; preferences.getPreferences(getContext(this), prefName) .then((p: preferences.Preferences) { pref p; }); function saveRecords(records: SurveyRecord[]) { if (!pref) { return; } let jsonStr JSON.stringify(records.map(item ({ id: item.id, studentName: item.studentName, gender: item.gender, activity: item.activity }))); pref.put(records, jsonStr); pref.flush(); }读取的时候从字符串解析回对象数组function loadRecords(): SurveyRecord[] { if (!pref) { return []; } let jsonStr pref.getSync(records, []) as string; let arr: SurveyRecord[] JSON.parse(jsonStr) as SurveyRecord[]; return arr; }我特别提醒一点SurveyRecord类里如果有方法JSON.stringify 不会保存方法解析出来的对象也只是纯数据对象。所以不要把“统计逻辑”放在这个类的方法里所有统计逻辑都放到页面或独立的纯函数中。这里再一次体现了“数据与计算展示分开”的原则。4.2 文本导出与分享传统复式统计表的作业通常要求学生抄一遍表。但数字化之后老师更想要的是“一键把结果变成一段可分享的文字”。我第一版做的是把统计表拼成纯文本复制到剪贴板function buildTableText( activityList: string[], genderList: string[], countMatrix: number[][] ): string { let lines: string[] []; let header 项目\t genderList.join(\t) \t合计; lines.push(复式统计表); lines.push(header); for (let i 0; i activityList.length; i) { let rowText activityList[i]; let rowTotal 0; for (let j 0; j genderList.length; j) { rowText \t countMatrix[j][i]; rowTotal countMatrix[j][i]; } rowText \t rowTotal; lines.push(rowText); } return lines.join(\n); }用\t分隔列粘贴到表格软件里能直接分列粘贴到聊天软件里也能对齐。我用这个方式最简单、最稳。系统分享接口虽然也能做但不同版本命名有差异V3 再考虑也不迟。5. 问题排查、性能优化和教学场景适配5.1 常见问题速查表我把这个实例开发中大概率遇到的问题整理成了表格照着查能省不少时间。现象原因解决思路点击提交后页面数字不变直接修改了嵌套数组元素状态管理器感知不到深拷贝二维数组后整体赋值给 State 变量List 记录显示顺序错乱或重复ForEach 没有使用稳定 key用 SurveyRecord 的 id 作为 key 生成函数表头跟单元格对不齐用 Row 手动拼接列宽不一致改用 Grid 的 columnsTemplate 统一分配列宽重启应用后数据丢失只存在内存里没有持久化用 Preferences 保存 JSON 字符串姓名输入为空也能提交没做输入校验提交前 trim 后判断长度为空时弹 Toast复数表格合计总数对不上计数矩阵只更新了一部分每次重算完整二维数组不要只修单个单元格这里我要反复强调 ForEach 的 key。代码里如果写成ForEach(this.records, (item: SurveyRecord) { ListItem() { ... } })第二参数不传 key 生成函数ArkUI 会默认用数组下标。这种情况在“只追加不删除”的时候没问题但一旦老师改错一条记录要删除UI 和底层数据的对应关系就会错位。更安全的写法是加上第三个参数ForEach( this.records, (item: SurveyRecord) { ListItem() { ... } }, (item: SurveyRecord) item.id )5.2 性能细节不需要过度优化这个场景数据量很小一个班撑死 60 条记录所以处理性能不需要大量技巧。但有两个地方值得注意。第一个是TextInput的 onChange 回调。如果要统计的数据量大了不要在 onChange 里每次都计算整张表只在“提交”按钮里做一次统计即可。我在开发时看到有人把统计逻辑塞进姓名输入事件里结果每敲一个字整个表格重算一遍体验很差。第二个是 Button 点击的防抖。班级里学生抢着操作可能连续快速点提交按钮。最简单的做法是提交后把按钮置灰 300 毫秒或者直接清空选项让下一次选择必须重新操作。我在代码里选择的方案是提交后清空姓名输入框并重置活动选项这样学生必须重新选择天然避免了连点造成的重复记录。5.3 这个实例后续还能怎么扩展如果你做完基础版还有余力我建议往三个方向扩展。第一个方向是加图表。用 Canvas 画一个简单的横向条形图横轴是活动项目柱高对应人数。注意图表的数据源不要重新算矩阵直接用已有的getGenderTotal和getActivityTotal函数这样能保证图表和表格永远一致。第二个方向是加“按班级分组”。Preferences 的一对 key 只适合保存一个班的数据。如果你想给多个班分别建表可以给记录增加className字段保存时用pref.put(className _records, jsonStr)。这是最简单、也最不容易出错的方案。第三个方向是调整成“不记名投票”模式。有些调查涉及隐私比如“你每天运动多久”这时可以把姓名输入框去掉只保留性别和活动选择。这个改动非常小但应用场景一下子扩大了。我在实际使用中发现不记名模式的调查速度更快因为学生不需要犹豫自己的名字会不会被看到。最后再分享一个小技巧我习惯在onPageShow里调用一次加载函数把 Preferences 里的数据重新读出来而不是只在应用启动时读一次。这样从后台切回来数据也保持最新老师误触退出后重新进入表格立刻恢复。这个习惯让我避免了很多次“数据怎么不见了”的误会。