基于ArkUI的HarmonyOS教育应用:如何实现“数位与组成”

发布时间:2026/10/12 13:23:07
基于ArkUI的HarmonyOS教育应用:如何实现“数位与组成”
这个标题乍一看像小学课本里的课后练习放成HarmonyOS应用实例却很有意思。“数位与组成”是低年级数学课堂里很核心的一个概念12不是简单的“1和2”而是“1个十和2个一”。让孩子亲手去摆一摆观察十位、个位上的珠子变化往往比背口诀更扎实。这个应用就是要在一台鸿蒙平板或者手机上把“摆一摆、想一想”的过程搬到屏幕上。我拿到这个题目时第一反应不是“又要写一个计数器”而是想到了数学课堂里常见的学具左边一捆小棒右边几根零散小棒下面摆一张数位表。孩子把十根一捆放到十位把散根放到个位看看一共是多少再反过来根据数字补充小棒。HarmonyOS上做同样的事不需要真买学具也不需要收拾桌面点一点珠子就完成一次摆放。这个例子适合两类人看一类是刚开始学ArkUI、想找完整应用练手的开发者另一类是想把数学课堂活动搬到电子设备上的教育类应用开发者。1. 应用定位先搞清楚这个“摆一摆”到底要教什么1.1 数位与组成看起来简单却容易教错很多成年人看“12”这个数下意识只会读出“十二”并不会去想它是由什么组成的。但对低年级孩子来说“十位上的1代表1个十个位上的2代表2个一”这个位值概念才是之后理解加减法、竖式计算和进位退位的根基。这个应用核心要解决的教学问题有三个第一孩子能不能把抽象的数字符号对应到“几个十、几个一”的具体数量上第二改变某个数位上的数量之后整个数会怎么变化第三反过来看到一个两位数时能不能准确说出它由几个十和几个一组成。这三件事看着基础却是许多孩子后续计算老出错的原因所在。“数位与组成”这个标题里的“数位”指的是个位、十位、百位这一类位置“组成”指的是“23由2个十和3个一组成”或“56由5个十和6个一组成”。应用如果只做一个摆数字的玩具意义不大。真正有价值的地方在于它能把“数位”和“组成”两件事同时呈现出来让孩子在摆的过程里看见数量结构而不是只记住答案。1.2 把“摆一摆”搬上屏幕的两种玩法我当时先整理了用户场景发现这类应用最常用的是两个模式。第一个是“自由摆”孩子可以在十位和个位的槽里随便加珠子屏幕上会实时显示组成文字比如“十位上有4颗个位上有5颗合起来是45”。这个过程对应标题里的“摆一摆”重点是让孩子通过自己的操作理解数量变化。第二个是“想一想”模式更像是闯关题。屏幕上方给出一个目标数字比如37孩子需要在下方摆出“3个十和7个一”。摆完后应用会判断结果并显示“37由3个十和7个一组成”。题目还可以设计成反向的用5颗珠子能摆出几个不同的两位数这时候孩子就不得不去想“珠子放在十位还是放个位代表的数完全不同”思考价值比直接给数字高得多。这两个模式相互配合正好对应“操作、表达、反思”的学习路径。先自由摆建立数感再按题摆验证理解最后做开放性题目促进深入思考。我把这三个环节都做进同一个页面不切到别的页面因为低年级孩子很容易在跳转中迷失方向单一页面反而更专注。1.3 为什么放在HarmonyOS上做选择HarmonyOS平台做这个教育类小应用原因很直接它是声明式UI写界面和状态联动时效率高。界面上的珠子和数字其实是同一份数据源的两种呈现改了数据UI会跟着刷新。这种开发方式恰好适合“数位与组成”这类应用因为珠子的摆放和数字的变化必须完全同步不能出现“摆了三颗珠子数字还停在12”这种错误。另外这类应用的主要使用场景是平板设备。孩子把平板横过来屏幕一边放十位槽一边放个位槽操作空间很充裕。HarmonyOS的多设备适配能力让一份代码可以跑在平板、折叠屏和手机上对家庭场景很友好。而且触屏的点击反馈天然适合低龄学生孩子不需要键盘鼠标直接用手指点珠子就能完成操作。2. 数据建模动手写代码前先把“状态”定下来2.1 用数组还是用两个独立变量在开始写ArkUI代码之前我习惯先想清楚数据模型。最自然的第一直觉是用两个变量tens和ones。这个写法在功能很简单的时候没有问题代码也好读。但问题是如果后续扩展出百位、千位就要再增加hundreds变量界面上的位置越多散落的变量就越多状态管理会变得琐碎。我最终采用了一个数组来表示所有数位State placeValues: number[] [0, 0];下标0表示十位下标1表示个位。这样一个结构就能同时表达多个位置加珠子、减珠子都可以通过下标操作。更重要的是后续扩展百位时只需要把这个数组变成三个元素不需要改动界面结构里的大部分逻辑。对于“数位与组成”这个主题数组比独立变量更贴合“位值”的抽象。这里有一个关键点数组是引用类型在ArkUI的声明式状态管理里直接修改数组元素并不一定触发UI刷新。更稳妥的做法是复制一份新数组修改后再整体赋值比如const next this.placeValues.slice(); next[index] 1; this.placeValues next;这样状态变更才清晰界面也会可靠地同步更新。2.2 值和组成文案不要存两份很多新手会习惯把“当前的数字”也存成一个状态变量比如State currentValue: number 0然后在加珠子时同时更新placeValues和currentValue。这种做法会埋下隐患一旦某个操作的更新顺序出错界面上显示的数字和珠子数量就会不一致。正确做法是只维护一份“真实数据”也就是placeValues。所有展示内容都从它实时计算出来。显示当前的数字时写一个方法getCurrentValue(): number { return this.placeValues[0] * 10 this.placeValues[1]; }显示组成文案时也从这个数组生成而不是保存一段字符串。比如“十位上有2颗个位上有5颗合起来是25”完全可以由placeValues拼接产生。这样无论用户怎么操作数据和展示始终不会脱节。这个原则听起来简单但实际调试时能省下大量排查时间。2.3 边界处理和“满十”问题加珠子和减珠子看起来只是数字加减实际上有几个边界需要提前想清楚。个位和十位的珠子范围都应该控制在0到9之间这既符合十进制表示也避免出现“个位有12颗”这种与位值概念矛盾的状态。我当时把加珠子逻辑写成一个通用方法哪个数位都能用public addToPlace(index: number): void { if (this.placeValues[index] 9) { // 这里可以触发一个“满了”的提示动效 return; } const next this.placeValues.slice(); next[index] 1; this.placeValues next; }这里并没有主动做“个位满十向十位进一”的自动进位。原因是对于低年级初学阶段最好让孩子先把“9个一再加1个一个位放不下”这个冲突暴露出来再由教师或家长引导思考“为什么要把10个一换成1个十”。如果应用自动把个位清零、十位加一孩子反而少了一次关键认知冲突。这个问题在设计阶段容易忽略因为它不是bug而是教学逻辑的选择。2.4 题目生成与正误判断“想一想”模式需要一套题目生成逻辑。最简单的做法是随机生成一个10到99之间的整数把它作为目标值然后让界面顶部显示这个数字。注意要把生成题目和清空珠子这两件事放在同一个方法里不然会出现“上一题剩了3颗这一题又从3颗开始”的尴尬局面。判断对错时有几条路。一种是比较目标值和当前值是否相等比如目标值是37当前摆出的值也是37就算正确。另一种是分别比较十位和个位上的珠子数量。答案比较简单时用第一种就行但我会更推荐第二种因为它能区分“数值结果对但结构没理解”的情况。比如目标值是20孩子如果理解成“二十”应该摆出十位2颗、个位0颗如果只比对结果20而某个位置放了2颗在个位数值显示是2也过不了逻辑上没问题但会弱化数位意识。所以判断方法写成这样checkAnswer(): boolean { return this.placeValues[0] Math.floor(this.target / 10) this.placeValues[1] this.target % 10; }同时比较十位和个位才是真正在验证“数位组成”这个概念是否掌握了。3. 用ArkUI落地核心界面与关键代码3.1 页面整体布局一个尽量少跳转的单页设计低年级孩子使用应用时最忌讳的是复杂导航。一个重要按钮按错了可能就回不到原来的界面。所以我把所有功能放进了同一屏顶部是题目或模式标题中间是两个数位槽下方是反馈文字旁边放清空按钮和判断按钮。用ArkUI写页面时整体结构是这样Column() { Text(this.mode challenge ? 请摆出 ${this.target} : 自由摆一摆) .fontSize(28) .fontWeight(FontWeight.Bold) Row() { this.placeColumn(0, 十位) this.placeColumn(1, 个位) } Text(this.getDescription()) .fontSize(22) Row() { Button(this.mode challenge ? 判断 : 清空) Button(换一题) } }这里用到了Builder来抽取“一个数位槽”的公共组件。因为十位和个位结构完全一样只是下标和标题不同写成公共方法后会清爽很多。具体的placeColumn方法内部会渲染标题、珠子区和调整数量的按钮。这种“一个组件描述同一类事物”的写法在HarmonyOS的声明式UI里特别自然。3.2 珠子的渲染用ForEach但别用内容当key珠子是这个应用的视觉主体我一开始想得比较简单在数位槽下面画一排圆形然后根据placeValues[index]的数值决定显示多少个实心圆。后来发现更大的问题是“空位”也需要画出来。如果只显示存在的珠子孩子点击时就没有一个明确的“放置目标”体验并不好。所以我改成把“空槽”也一起画出来一排9个位置已经有珠子的显示实心圆没珠子的显示浅色空心圆。这样孩子能看到“我这个槽还能放几个”对十位最多9个、个位最多9个的边界也能形成直观感知。为了用ForEach渲染我需要先把数字placeValues[index]转成一个长度固定为9的数组里面的每个元素表示当前这个位置是否有珠子createBeads(count: number): number[] { let result: number[] []; for (let i 0; i 9; i) { result.push(i count ? 1 : 0); } return result; }然后用ForEach遍历这个数组生成圆形。这里有一个很容易踩的坑ForEach的第三个参数是key生成器。我之前写过用内容当key比如item.toString()但问题是这个数组里可能有多个“1”key就重复了导致渲染异常。正确定法是用索引当keyForEach(this.createBeads(this.placeValues[index]), (item: number, beadIndex: number) { Circle({ width: 30, height: 30 }) .fill(item 1 ? #e67e22 : #e0e0e0) }, (item: number, beadIndex: number) beadIndex.toString())用索引生成的key在数量变化时是稳定的一个一个增加或减少都不会乱序。3.3 点击交互加珠、减珠与防误触珠子的交互方式我前后改过两次。第一次是点十位区域就加一颗点个位区域就减一颗用一个模式切换按钮来控制加减模式。但是低年级孩子会忘记“现在是加还是减”反而绕。第二次改成更直觉的规则点一个空槽就往这个槽里放一颗珠子点一个有珠子的圆就把它拿下来。这个规则和物理世界中的“摆放”动作一致也更符合“摆一摆”的主题。对应的点击逻辑是tapBead(placeIndex: number, slotHasBead: number): void { const next this.placeValues.slice(); if (slotHasBead 1) { next[placeIndex] - 1; } else { if (next[placeIndex] 9) { return; } next[placeIndex] 1; } this.placeValues next; }这个逻辑有一个好处孩子不会出现“想从9个变成10个”越界的操作因为第10个空位本身就不存在只画9个圆。这也是为什么要把空槽画满9个位置它比单独一个“加号按钮”更能传递“每一位最多9”的数位限制。3.4 挑战模式的反馈和“换一题”逻辑Challenge模式里孩子摆完珠子后要点击“判断”按钮。我建议在判断结果出来后不要去弹窗打断操作。低年级孩子坐在前台时弹窗容易误触而且关闭弹窗的按钮对不识字的儿童并不友好。更好的反馈是直接用反馈区的颜色和文字变化比如正确时反馈区变成浅绿色错误时变成浅红色并显示当前摆出的数字与目标数字的组成差异。“换一题”按钮需要做两件事调整目标数字以及清空所有珠子。这个联动很关键我当时就在这里犯过错单独点了“换一题”结果新旧题目混在一起判断很久才发现目标已经变了珠子却还是上一轮的。后来把生成题目封装成一个方法同时重置placeValues问题才解决。nextQuestion(): void { this.target Math.floor(Math.random() * 90) 10; this.placeValues [0, 0]; }目标值尽量排除整十数比如10、20、30这类因为它们会让孩子在判断“十位上有几颗、个位上有几颗”时偶尔出现零的混淆。等基础题型熟练之后再把整十数加进来作为进阶题。4. 开发中容易踩的坑这些都是一次次试出来的4.1 ForEach不刷新十有八九是key的问题在HarmonyOS的ForEach里key生成器不是可有可无的参数。如果省略或者随便写界面更新时可能出现两种现象一是珠子数量变化了但界面没有同步二是某个位置的圆突然消失。绝大部分情况下问题都出在重复key上。我之前在另外一个组件里用“珠子的颜色”当key结果一排圆的颜色只有两种key全部冲突界面直接乱掉。ForEach遇到相同key时无法区分新旧条目干脆不更新。所以我的建议很简单列表项的位置有明确顺序时用索引当key基本不会出错。你要是用了beadIndex.toString()就一定别把item当key。4.2 数字显示不更新源于复制原生数组的方式不对另一个容易忽略的问题是数组的不可变性。直接执行this.placeValues[index] 1后虽然数组内容变了但状态管理无法检测到引用变化界面就不会刷新。这是我第一次测试时遇到的经典问题逻辑算出的数字是对的但界面纹丝不动。解决办法就是我在前面提到的slice()赋值。每次要改数据先复制新数组修改新数组再把新数组赋给状态变量。这个习惯建立起来之后几乎不会再出现“数据变了但界面不刷新”的鬼故事。其实这不只是HarmonyOS的问题很多声明式框架都是这种思路。多写几行代码换来的是状态流的确定性值得。4.3 手机竖屏下珠子和数字会被挤变形平板横屏下两个位槽并排完全没有问题。但手机竖屏时宽度有限如果每个数位槽内部再用很宽的圆和间距很容易出现一行放不下、按钮被挤到屏幕外的情况。我第一次只在平板上测没考虑手机竖屏结果一换设备布局就乱。后来我做了两个调整。第一所有尺寸都使用相对单位圆的大小和间距用vp的百分比经验值而不是写死像素。第二在不改变结构的前提下把“文字反馈区”从底部挪到两个数位槽中间让它作为自然分隔这样即使屏幕变窄布局也不会拥挤。更重要的是我用layoutWeight给两个数位槽分配相等宽度它们会等分父容器的可用空间不会因为文字长短而一边宽一边窄。4.4 连续点击导致动画和判定错乱低年级孩子用手指点屏幕时动作往往又快又急连续点击很常见。如果判断按钮的回调里执行了动画或弹窗还来不及响应下一次点击状态就会混乱。这里我加了简单的防抖逻辑判断开始后让“判断”按钮在0.5秒内不可再次点击换题时也做同样处理。注意不要依赖sleep或者复杂的时间控制。更好的做法是用一个状态标记State isJudging: boolean false; judge(): void { if (this.isJudging) { return; } this.isJudging true; // 执行判断、更新反馈 setTimeout(() { this.isJudging false; }, 600); }这行代码并不复杂但功能表现会稳定很多。不要小看这个细节实际拿到孩子面前试一圈就会知道连续点击是常态不是异常。4.5 模式切换时残留旧状态自由摆和挑战模式共存时很容易出现一种情况孩子在自由模式摆了三颗珠子切到挑战模式后这三颗珠子还在。这个在成人看起来是 bug在低年级场景里就是教学事故。孩子可能会以为“上一题我摆的就是这个答案”结果连点判断都错了。最后的处理方案是在模式切换方法里统一重置状态无论从哪个模式切到另一个都调用resetGame()。这样规则就变得很简单新题目一定从空槽开始公平又清楚。5. 后续还能怎么扩展这是我个人的一些想法5.1 增加百位和“整捆”操作当前版本主要解决两位数的数位与组成但应用扩展空间很大。最直接的方向是引入百位把placeValues从两个元素变成三个元素并在百位槽里用“一捆小棒”的图形来替代散珠。点击十位的时候如果能先放满10颗再触发“整十换一捆”的动画孩子就能直观看到十位和百位之间的满十进一关系。这种视觉化操作远比纯文字更有冲击力。扩展时不用改太多逻辑因为一开始就用了数组来管理数位。这也是我当时坚持用数组而不是tens、ones两个变量的原因。后续叠加百位、千位只是数组长度变长判断和生成题目的核心代码不变。5.2 把练习变成闯关和打卡教育类应用最怕没有情感连接。如果只是重复摆数字孩子热情会很快消减。我会在现有基础上加一个非常轻量的闯关记录每完成一组题就点亮一颗小星星。星星只记录“今天完成了哪些数位组合”不做失败惩罚也不显示“错了几次”。这样既保留激励感又不会让学习变成压力。另外可以记录一个“最近练习数位”的列表比如今天练了25、37、46下次进入时优先从这个列表里出题。这个功能不复杂但对家长来说很有用相当于可以看到孩子的练习轨迹。实现时要注意数据存储的私有性尽量只存在本地避免不必要的联网权限。5.3 结合语音和更真实的学具样式低年级孩子识字量有限屏幕上全是汉字反而增加认知负担。我计划在后续版本里给关键反馈增加语音播报比如摆完珠子后自动播放“两个十和三个一合起来是二十三”。对还不认识数字名称的孩子来说语音辅助能让他们把声音和符号对应起来。这个功能用系统自带的语音能力就能实现不用外接语音服务。界面样式也可以继续打磨。把圆点珠子换成小棒或者卡通积木整体视觉更贴近真实学具。需要特别强调的是视觉样式不能影响数位的边界认知十位和个位还是要把区域分隔得足够明显避免孩子产生混淆。如果你也准备做这类教育应用我的个人建议是先别急着加一堆功能。把“摆一摆、想一想”两个动作做扎实比做十个花哨页面更能帮到孩子。开发时多拿真机试触控多留意珠子的位置变化和数字刷新是否同步这些细节才是这个例子真正值钱的部分。希望这篇记录能让你少踩几个坑。