
在HarmonyOS应用实例系列整理到第78个的时候我越来越觉得一件事真正能帮到新手的不是那种堆满图表、地图、多模块的“大而全”项目而是一个把真实场景做清楚、把数据逻辑讲明白的“小而精”案例。这次分享的“平均数数据调查活动”就是这样一个典型场景——你在班级里统计同学们每周的运动时长在家庭群里收集大家的作息数据或者在小团队里调研每天加班多少小时本质上都是一类需求录入一组调查数据实时算平均值还能随时修正或删除某条数据。今天我就用HarmonyOS的ArkTS ArkUI把这套东西完整实现一遍从界面搭建到状态管理再到边界处理和扩展方向全部讲透。这个实例适合正在学HarmonyOS声明式开发的初学者也适合已经把基础组件过了一遍、想看看完整业务逻辑怎么串起来的开发者。你不仅会看到一个平均数计算器还会看到一个数据类应用最基础也最关键的骨架数据怎么录入、状态怎么维护、列表怎么渲染、结果怎么联动。搞懂这一套后面做问卷、记账、评分统计都是顺手的事。1. 项目拆解平均数数据调查活动到底在做什么1.1 场景需求为什么用平均数做一个HarmonyOS应用先说场景。数据调查不是一个陌生的概念学校里做体质测试统计、公司里填问卷收集OKR数据、小区里调研垃圾分类参与率最终都要落到几个核心问题上面一共收集了多少条数据、总和是多少、平均值是多少、哪些数据异常需要剔除。平均数这个统计量刚好是“数据调查”最直观的切入点。它不需要复杂的概率分布知识总共就两行计算总和除以个数。但正因为简单它对应用的容错性、交互流畅度和数据管理能力提出了更明确的要求——你输进去一条非数字内容怎么办一条数据录错了怎么改平均值除不尽怎么展示这些细节处理得好不好才是一个应用能不能真正落地使用的分水岭。所以我在设计这个项目时没有把它只当成一个“算平均数的计算器”来做而是对标一个微型调查系统。功能上至少要包含数据录入、数据列表展示、单条删除、一键清空、实时平均显示、录入不合法时的提示以及数据为空时的占位反馈。这样用户拿过去就能直接用而不是一个只能演示、不能实战的玩具。1.2 功能清单与页面规划把需求转成界面其实就是“上中下”三个区域顶部是标题和数据录入中间是已录入数据列表底部是统计结果和操作按钮。这是数据类应用非常经典的信息架构信息流方向也是从上到下输入 → 展示 → 汇总符合大多数人的操作直觉。具体功能清单我整理成下面这张表开发前先列清楚干活就不容易跑偏功能模块具体功能交互说明数据录入输入调查数据数字键盘支持小数过滤非法字符数据列表展示已录数据带序号支持逐条删除统计结果实时显示平均值数据为空时显示占位文案数据管理清空全部数据带二次确认或一键清空校验反馈非法输入提示空输入、非数字、极端值处理你可能会问删一条数据要不要二次确认我的建议是单个删除不必弹窗确认但“清空全部”建议保留一步确认逻辑或者至少让用户能明确意识到这是不可逆操作。后面我会讲到实现细节。1.3 技术选型ArkTS ArkUI的声明式开发优势技术选型上HarmonyOS应用开发现在的主流方式是ArkTS语言搭配ArkUI声明式框架。所谓“声明式”核心思路是你只需要描述界面长什么样状态是什么框架负责在状态变化时去更新界面而不需要你手动操作每一个组件的文本和属性。传统的命令式开发比如早期安卓里的XML findViewById改一个数字要先找到控件再把计算好的值set进去声明式开发的做法是你在build里声明“这里放一个Text显示的内容绑定到avgText状态”只要avgText变了界面自动就变了。这个思路对数据调查这种“频繁增删数据、结果实时联动”的场景非常匹配开发效率和可维护性都明显更好。2. 界面搭建ArkUI组件用得顺不顺手2.1 整体布局Column Row组合出来的表单页页面整体用垂直布局Column撑起来这是ArkUI页面最基础的骨架。内部再根据需要嵌套Row做水平排列用List承载动态数据列表用分区组件把“输入区、列表区、结果区”三个视觉区块分开。我习惯先搭外框再填组件最外层一个Column占满全屏内部分成四个子区域——顶部标题占固定高度输入区固定高度列表区用layoutWeight(1)占掉剩余空间这样列表长了能滚动底部结果区固定高度。这个“固定 弹性 固定”的组合在真机上非常稳不管屏幕多大列表区都能自动伸缩。有一点值得提醒别把整个页面做成Scroll嵌套ListArkUI里List本身就支持滚动嵌套Scroll容易产生滑动冲突和性能问题。列表数据多了以后滚动上List也比Scroll流畅得多因为它有懒加载和复用机制。2.2 输入与按钮TextInput和Button的协作细节录入数据用的是TextInput组件有一个很实用的属性type(InputType.Number)。设置了之后真机上弹出的就是数字键盘用户就不用再手动切换输入法录入体验会好很多。这一点在做数据类应用时非常关键我见过不少初学者的应用没设置这个用户每次录入都要切键盘很不方便。不过数字键盘只影响软键盘的形态不能完全避免用户输入非法内容。有些人会在输入法里给出全角数字、小数点和特殊符号所以代码里的校验逻辑仍然不能省。我在TextInput的onChange回调里把当前输入值保存到inputValue状态添加按钮被点击时再做统一校验而不是每次按键都校验这样交互更顺畅也给用户留了修改输入的空间。按钮这里用Button(添加)点击回调里执行addData()。ArkUI的Button本身有多种样式可以直接通过backgroundColor、fontColor、borderRadius这些属性调整外观不用额外包一层容器。2.3 数据列表ForEach渲染与key生成器列表渲染是声明式UI的核心环节。我在List里嵌套ForEach遍历dataList数组每一条数据生成一个ListItem里面放一组Row左侧序号和数值右侧删除按钮。用户点击删除时根据索引index调用removeData(index)把对应数据移除。ForEach的第三个参数是key生成器很多人会忽略它但这个参数在数据重复或顺序变化时特别重要。我用的key生成器是(index) ${index}-${item}把索引和值拼在一起保证每条数据的key唯一且稳定。如果不写key生成器在一些场景下ArkUI自己生成的key可能不够稳列表项复用后会出现状态错乱、动画异常之类的问题排查起来很费劲。宁可在写代码的时候多花两秒把key生成器写明白。3. 核心逻辑平均数的计算与状态管理3.1 State状态管理数据一变界面就变的原理先说你必须要理解的一个概念State。在ArkUI里状态就是“会驱动界面变化的数据”。我用了三个State变量dataList存已经录入的所有数据类型是number数组inputValue存输入框当前文本avgText存界面展示的平均结果。每次这些状态变化依赖它们的组件会自动重建或更新。有个很重要的原理State监听的是引用变化。对数组来说你直接去改某个下标的值比如this.dataList[0] 100界面通常不会刷新因为数组本身的引用没有变框架感知不到发生了修改。正确的做法是给数组赋一个全新的引用用扩展运算符this.dataList [...this.dataList, value]或者用filter、map这类返回新数组的方法来更新。这个坑我在刚开始写HarmonyOS时踩过好几次一度以为UI没刷新是框架的Bug查了半天结果是自己没有替换引用。你记住一句话数组更新永远给它一个新数组。3.2 添加数据与合法性校验添加数据看起来就是把输入框内容转成数字然后push进数组但实际做的时候要处理三个边界。第一个是空输入。用户点了添加但什么都没输如果直接Number()会得到0你以为录入了一条0的数据实际是非法的。所以第一步先对原始字符串做trim判断是不是空字符串。第二个是非数字。Number(abc)得到NaN所以用isNaN方法做判断。这里还要注意isNaN(Number( ))为false但字符串本身是空白所以空值的判断一定要放在数字转换之前顺序不能反。第三个是重复点击。如果你的添加按钮没有做防重复处理用户快速连点两次就可能添加两条同样的数据。如果业务上允许重复那无所谓如果每条数据都代表一个独立样本那可以在添加前检查数组中是否存在相同值存在就提示“重复数据”并中断添加。我这个场景里允许重复就没有做这层限制但判断逻辑不难留着注释你自己决定要不要加。3.3 平均数计算与展示细节平均数计算的公式就是总和除以个数我单独封装了一个updateAverage()方法这样每次列表变化后调用一次即可。计算时先判断数组长度是否为0为空时直接把avgText设置成“暂无数据”避免0/0得到NaN。除不尽的情况是很多新手忽略的。比如3条数据分别为1、2、4平均数是2.33333……直接拿结果去toString会显示一大串小数。这里我做了一层格式化先用toFixed(2)保留两位小数再用parseFloat包一层去掉末尾多余的0最终得到的avgText会显示成2.33而不是2.330000000001这样的样子。这里还要特别说一下浮点精度问题JavaScript的浮点数在计算0.10.2时会有误差连加多个小数后误差可能累积所以平均值展示前做一次toFixed格式化是非常有必要的。4. 实操过程把调查活动跑起来的完整步骤4.1 工程准备与页面骨架实际操作层面打开DevEco Studio新建一个Empty Ability工程语言选ArkTS兼容API版本建议选当前稳定版本API 11及以上。工程建好后默认的Index.ets就是我们写页面代码的文件。页面骨架用Entry Component描述。Entry标记这是一个页面入口Component标记这是一个自定义组件struct里的build方法描述界面树。下面先写一个只含骨架的版本把页面分区都搭出来跑一遍确认布局没问题再往里填逻辑。Entry Component struct AverageSurvey { build() { Column() { Text(数据调查 · 平均数计算) .fontSize(24) .fontWeight(FontWeight.Bold) .margin({ top: 20 }) Row() { TextInput({ placeholder: 请输入调查数据 }) .layoutWeight(1) Button(添加) } .padding(16) List() { } .layoutWeight(1) Column() { Text(调查结果) Text(暂无数据) } } } }4.2 完整代码与逐段解析骨架确认没问题后把状态、逻辑、事件补全就是下面这版完整实现。代码不长但每块都有值得品一下的地方。Entry Component struct AverageSurvey { State dataList: number[] [] State inputValue: string State avgText: string 暂无数据 State tipText: string build() { Column() { // 顶部标题区 Text(数据调查 · 平均数计算) .fontSize(24) .fontWeight(FontWeight.Bold) .margin({ top: 20 }) Text(输入调查数据自动统计平均值) .fontSize(14) .fontColor(#666666) .margin({ top: 8 }) // 输入区 Row() { TextInput({ placeholder: 请输入调查数据 }) .type(InputType.Number) .layoutWeight(1) .onChange((value: string) { this.inputValue value }) Button(添加) .margin({ left: 12 }) .onClick(() { this.addData() }) } .width(100%) .padding(16) // 错误提示区 if (this.tipText ! ) { Text(this.tipText) .fontSize(12) .fontColor(#E84026) .margin({ left: 16, right: 16 }) } // 数据列表区 List() { ForEach(this.dataList, (item: number, index: number) { ListItem() { Row() { Text(第${index 1}条数据) .fontSize(16) Blank() Text(${item}) .fontSize(16) .fontWeight(FontWeight.Medium) Button(删除) .backgroundColor(#E84026) .fontSize(14) .onClick(() { this.removeData(index) }) } .width(100%) .padding(12) .borderRadius(8) .backgroundColor(#F5F5F5) .margin({ bottom: 8 }) } }, (item: number, index: number) ${index}-${item}) } .width(100%) .layoutWeight(1) .padding(16) // 结果区 Column() { Text(调查结果) .fontSize(16) .fontColor(#666666) Text(this.avgText) .fontSize(32) .fontWeight(FontWeight.Bold) .margin({ top: 8 }) Text(共录入 ${this.dataList.length} 条数据) .fontSize(14) .fontColor(#888888) .margin({ top: 4 }) } .width(100%) .padding(20) .backgroundColor(#F0F8FF) .borderRadius(12) .margin({ left: 16, right: 16, bottom: 16 }) // 清空按钮 Button(清空全部数据) .backgroundColor(#999999) .width(80%) .onClick(() { this.clearAll() }) .margin({ bottom: 24 }) } .width(100%) .height(100%) } // 添加一条数据并更新平均值 addData() { const trimmed this.inputValue.trim() if (trimmed ) { this.tipText 请先输入数据 return } const value Number(trimmed) if (isNaN(value)) { this.tipText 请输入合法的数字 return } this.dataList [...this.dataList, value] this.inputValue this.tipText this.updateAverage() } // 删除指定索引的数据并更新平均值 removeData(index: number) { this.dataList this.dataList.filter((_, i) i ! index) this.updateAverage() } // 清空所有数据 clearAll() { this.dataList [] this.inputValue this.tipText this.avgText 暂无数据 } // 计算并更新平均值 updateAverage() { if (this.dataList.length 0) { this.avgText 暂无数据 return } const sum this.dataList.reduce((acc, cur) acc cur, 0) const avg sum / this.dataList.length this.avgText parseFloat(avg.toFixed(2)).toString() } }4.3 运行效果与交互流场这段代码直接跑起来后整条交互流是通顺的。用户输入一个数字点添加数字以新列表项的形式出现在中间列表区底部平均结果实时更新录入条数同步变化。如果输入为空或输入了非数字内容输入框下方会出现一行红字提示提示文案会在下一次成功添加后自动消失。点列表项右侧的删除按钮对应数据移除平均值立即重算。数据全部删除后底部显示“暂无数据”同时“共录入 0 条数据”。整个过程中没有一个环节需要用户等待也没有任何手动的“刷新”或“提交”动作。好的数据调查应用就应该这样所见即所得录一条算一条改一条结果跟着变。5. 常见问题与避坑指南5.1 数字键盘与输入类型有朋友反映设置了type(InputType.Number)之后真机弹出来的仍然是全键盘或者键盘弹不出来。这种情况一般有三个可能一是API版本比较老建议升级到API 9以上二是模拟器对数字键盘支持不够好建议直接在真机上验证三是TextInput处于List内部键盘弹出时被遮挡需要配合adjustKeyboardToCursor属性调整键盘避让方式。实操中最省事的做法是先真机跑把type设置到位再检查页面里有没有高度固定导致键盘遮住输入框。5.2 数组刷新不生效的坑这是DataList类应用最容易出的问题。你写了this.dataList[index] value或者this.dataList this.dataList界面纹丝不动。原因前面说过State监听的是数组引用变化。改成用扩展运算符、filter、map、concat这些方法返回新数组问题立刻消失。我把一个现象记忆法分享给你ArkTS里的数组没有“原地修改”概念每次更新都是“换盒”不拆盒换盒不行。另外还要注意在ForEach里去修改item本身是无效的因为item是ForEach回调里的副本改它不会改到真实数据源。要改数据一定回到对dataList的更新操作上。5.3 计算精度与大数据量问题平均数显示成2.3300000000000001这种事新手第一次遇到会以为是计算器坏了其实是浮点数精度问题。处理方式我上面已经写了用toFixed保留小数位再parseFloat去尾零。大数据量方面如果调查活动一次要录入上千条数据每次添加都用[...this.dataList, value]这种展开方式做拷贝性能和内存会有点压力。但作为普通调查场景几百条数据完全无压力。真要做到几万条级别应该考虑用LazyForEach代替ForEach做列表懒加载同时把数据持久化到本地数据库而不是全部存在内存里。这个扩展方向不展开写了记住一句话作为预案数据量上来之后分页加载 懒加载是标配。6. 扩展思考从平均数到完整数据调查工具6.1 增加图表与统计指标平均数只是第一步。做一次数据调查用户通常还想看更多统计量比如最大值、最小值、中位数、众数、标准差。这些计算逻辑本身不复杂都是在现有dataList上做遍历和排序扩展起来非常顺手。界面层面可以在结果区加一排横向滚动的指标卡片每个卡片显示一个统计量名称和数值。图表方面HarmonyOS有丰富的图表库可以直接集成柱状图可以展示数据分布折线图可以看趋势。给这个项目接一个简单的柱状图组件把dataList里的数据变成柱形整个应用的完成度会提升一大截看起来就是一个正经的数据分析工具了。6.2 数据持久化与多轮调查现在的版本有一个局限数据存在内存里退出应用就没了。真实调查场景里数据收集到一半被系统杀掉用户心态会崩。所以要接入本地持久化。HarmonyOS里最简单的方式是使用Preferences轻量级存储把dataList序列化成JSON字符串存进本地如果想更规范一些可以用关系型数据库RDB建一张调查表字段包括id、数值、录入时间。再加上时间字段后就能支持多轮调查用户选择某一轮次查看历史平均数据跨session的完整数据流就落地了。我在实际把这类应用做成长期可用的工具时感触最深的是设计层面的事计算平均数本身很简单但怎么让用户录错了能改、录入无效数据有反馈、数据量大不卡、退出应用数据不丢这些才是决定工具成败的细节。你是不是真正站在用户使用的流畅度上思考过每个交互一眼就能在你的页面上看出来。最后再分享一个小技巧给列表项加上入场动画和删除动画会让交互柔和很多。Flutter那边有AnimatedListHarmonyOS里则可以用transition属性去控制ListItem的出场退场效果。别小看这几百毫秒的动画数据调查应用在多人配合录入时视觉反馈能让使用者明显感觉“数据被系统接住了”参与录入的人心理体验会好很多。这也是我整理到第78个实例后越来越看重的东西——功能能跑只是起点把细节打磨到位才是工程经验真正的价值。