
1. 项目概述最近在做 HarmonyOS 应用开发练手项目合集写到第 116 个实例的时候选了一个既简单又能把逻辑讲透的主题鸽巢原理模拟器。这个项目的名字听起来有点学术实际上做出来就是一个轻量级的交互式教学工具适合放在教育类应用里做数学原理演示也可以作为 ArkTS 声明式开发的入门练手项目。鸽巢原理在数学里也叫抽屉原理核心表述只有一句话如果把 n1 个物体放进 n 个抽屉那么至少有一个抽屉里会有两个或以上的物体。这个原理本身极其简单但它是组合数学、图论、数论里很多证明的根基。我做的这个模拟器目标就是把这个原理变成可视化的交互过程用户输入鸽子和鸽巢的数量应用自动判断是否满足鸽巢原理的条件然后用图形化的方式展示分配结果让用户直观看到“至少有一个巢里挤进了两只鸽子”这个结论是怎么来的。这个项目适合三类人看一是正在学 HarmonyOS 开发、想找一个完整 ArkTS 状态管理案例的初学者二是做教育类应用的开发者想参考这类数学原理演示工具的实现思路三是对鸽巢原理感兴趣、想做一个教学小工具的非专业开发者。整个应用的代码量不大核心逻辑集中在状态管理和自定义绘图上但麻雀虽小五脏俱全输入校验、动态布局、随机算法、弹窗交互这些常规开发环节都覆盖到了。我在设计这个项目的时候刻意让它保持“教学演示”的气质而不是做一个花哨的游戏。界面上一开始就是两个输入框和两个按钮底部是鸽巢和鸽子的展示区域。用户输入数量和点击操作之间的反馈链非常短几乎即时响应这样能让用户把注意力集中在鸽巢原理本身的逻辑上而不是被复杂的操作流程干扰。从实际体验来看这个应用在平板上演示效果最好手机上也完全可用只是展示区域会紧凑一些。2. 核心设计与技术选型思路2.1 为什么选择 ArkTS 声明式开发HarmonyOS 应用开发目前主推的是 ArkTS ArkUI 的声明式范式。这个项目的界面结构并不复杂但交互状态倒是有几个输入值、鸽巢数据、鸽子数据、结果弹窗、颜色状态、位置记录。如果用传统的命令式写法这些状态分散在不同地方改起来容易漏。ArkTS 的声明式特点恰好适合这种场景状态变化自动驱动 UI 刷新省去了手动操作组件的繁琐步骤。举个例子用户点击“自动填充”按钮后系统需要把若干只鸽子随机分配进鸽巢如果每只鸽子的位置都要手动用代码去找对应的 UI 组件来修改那代码会非常啰嗦。用 ArkTS 的话只需要维护一个数组数组一变界面上的 ForEach 循环自动重新渲染开发效率高得多。这也是我在这个合集里坚持用 ArkTS 写教学类应用的原因——逻辑清晰代码量少读者跟着看的时候不容易迷路。2.2 鸽巢原理判断逻辑的数学基础这个应用的核心判断逻辑其实就一条规则当鸽子的数量 m 大于鸽巢的数量 n 时必然存在至少一个鸽巢中装有两只或以上鸽子。这个规则不是模糊的经验判断而是严格的数学结论所以程序里的判断条件只需要比较两个输入数值的大小即可。但这里有一个容易踩坑的地方边界情况。如果鸽子的数量等于鸽巢的数量比如 5 只鸽子 5 个巢那每个巢装一只刚好装满不存在“至少一个巢有两只”的情况这时候程序应该给出“不满足鸽巢原理条件”的提示。如果鸽子的数量小于鸽巢的数量有空巢是正常情况也不满足原理条件。只有 m 严格大于 n 的时候才触发“必然有重叠”的结论。我在代码里把判断函数的逻辑写得尽量直白避免把简单的事情复杂化。实际开发中很多新手容易在这个环节加一些不必要的分支判断其实只需要一个if (pigeons nests)就够了。复杂化往往不是因为需求复杂而是因为思路没理清。2.3 界面布局的取舍卡片式 vs 自由式这个模拟器的展示区域有两种主流设计思路。第一种是卡片式每个鸽巢是一个卡片鸽子显示在卡片内部鸽巢数量改变时卡片自动换行排列。第二种是自由式整个展示区是一块画布鸽子和鸽巢用坐标定位位置随机分布。我最终选了第一种原因有三个。第一卡片式的布局代码量更少用 ArkUI 的Grid或Flex容器加上ForEach循环就能搞定不需要手动计算坐标也不会出现元素重叠的问题。第二卡片式的视觉效果更清晰用户一眼就能看出哪个巢里有几只鸽子这对教学演示来说比“自由散落”的效果更有说服力。第三卡片式天然支持动态增减数量鸽巢数量从 3 个改成 8 个卡片自动重新排列不需要额外处理坐标变化。自由式布局虽然视觉效果更生动但对数学演示来说反而容易分散注意力。鸽巢原理的核心是“分配”和“重叠”卡片式的结构化展示更能突出这两个要素。3. 详细设计与界面交互实现3.1 输入区域设计界面顶部是一个输入区域包含两个输入框、两个按钮和一个状态提示。第一个输入框是“鸽巢数量”第二个输入框是“鸽子数量”。两个输入框都用了TextInput组件通过type: InputType.Number限制用户只能输入数字。输入框的校验逻辑是整个项目里最容易出问题的环节。我一开始只校验了“是否为空”没有处理“是否为 0”和“是否为负数”的情况。后来测试发现用户输入 0 的时候程序会直接崩溃因为后续要生成长度为 0 的数组某些数组操作在边界条件下会抛异常。这个问题充分说明输入校验永远不只是判断空值而是要覆盖所有可能的非法输入。最终我加了三层校验第一层判断是否为空为空则弹出提示“请输入数量”第二层判断是否为 0 或负数提示“数量必须大于 0”第三层限制最大值鸽巢数量不能超过 50鸽子数量不能超过 100防止用户输入极端值导致界面渲染卡顿。这三个限制条件都有实际意义0 会让后续逻辑失效负数无意义超大数字会让卡片区域滚动卡顿。实际项目中的输入校验远比教学示例复杂但这个三层校验的思路可以迁移到大部分表单场景。两个按钮分别是“自动填充”和“手动分配”。自动填充模式下程序会随机把鸽子分配到各个鸽巢手动分配模式下用户每点击一次鸽子鸽子就移动到下一个鸽巢。手动分配模式是后加的一开始只设计了自动填充按钮后来觉得教学场景里“用户自己动手操作”更重要就加了这个模式。实际做出来的效果很好手动操作的过程本身就是理解鸽巢原理的过程。3.2 数据模型与状态管理这个应用的核心状态有三个鸽巢数组、鸽子数组、操作标志位。我分别设计了两个接口来管理这些数据。先看鸽巢的数据结构。每个鸽巢需要记录三样信息编号、当前装了几只鸽子、颜色。颜色不是必须的但加上之后视觉效果会好很多用户可以直观地看到哪个巢的鸽子多。鸽巢接口定义为interface NestItem { id: number; // 鸽巢编号 count: number; // 当前鸽子数量 color: string; // 显示颜色 }鸽子的数据结构更简单每只鸽子只需要编号和位置两个字段。位置用巢的编号来表示初始值为 -1意思是“还没分配”interface PigeonItem { id: number; // 鸽子编号 nestId: number; // 所在鸽巢编号-1 表示未分配 }状态管理用State装饰器。State是 ArkTS 中最常用的响应式装饰器被它修饰的变量发生变化时UI 会自动刷新。在这个项目里nests数组和pigeons数组都是State类型这样每次分配操作后界面就能即时更新。这个机制是 ArkUI 声明式开发的灵魂理解了它后面所有界面刷新问题都迎刃而解。这里有一个我在实际开发中反复测试过的细节State装饰的数组在修改时必须保证是一个新的数组引用才能触发 UI 更新。直接通过索引修改数组元素比如this.pigeons[0].nestId 2UI 可能不会刷新因为 ArkUI 的State监听的是赋值操作而不是对象的属性变化。正确做法是先用一个新的数组接收修改结果再整体赋给状态变量。我在代码里都严格遵循了这个规则这也是为什么分配逻辑里经常出现“创建一个新数组修改后再赋值回去”的写法。3.3 鸽巢展示区的卡片渲染展示区域是整个应用的主体部分我用Grid容器来搭建卡片布局。每一列代表一个鸽巢具体显示效果通过自定义子组件NestCard实现。NestCard接收一个NestItem对象渲染出鸽巢的边框、编号和内部的鸽子图标。卡片的高度不宜固定写死因为鸽子数量变化时卡片内容会变多。我在NestCard里用Stack布局做层叠效果鸽巢背景在最底层鸽子的图标叠在背景上方。每张卡片内部用Column容器管理垂直排列的鸽子图标鸽子数量多时自动增加高度量少时保持紧凑。这个自适应逻辑在 ArkTS 里天然支持只要容器不设置固定高度子组件就能自动撑开。鸽巢数量多的时候比如 30 个展示区需要滚动才能看到全部内容。我给Grid外层包了一个Scroll容器让整个展示区支持纵向滚动。这里有个细节Scroll容器内部的Grid需要设置明确的列数和间距否则滚动高度计算会出问题。列数方面手机竖屏时 2 列比较舒适平板横屏时 4 列或者 6 列更好。我做了响应式处理根据屏幕宽度动态计算列数这个在Grid的columnsTemplate属性里设置字符串模板即可实现。3.4 自动填充功能的随机分配算法自动填充的核心是一个随机分配循环。程序要做的操作是遍历每一只鸽子随机给它分配一个鸽巢编号然后把鸽巢里的鸽子数量加 1。这个逻辑听起来简单实现的时候有几个容易出错的地方。第一个坑是随机数生成范围。HarmonyOS 的 ArkTS 里生成随机数用Math.random()它返回 0 到 1 之间的浮点数。要生成 0 到 n-1 之间的整数标准写法是Math.floor(Math.random() * n)。这里比较容易忽略的点是Math.random()永远取不到 1所以乘上 n 之后最大只会接近 n取整后最大是 n-1不会越界。但如果少写一个Math.floor直接强转整型某些语言会直接截断而不是取整容易出现分布不均的问题。好在 ArkTS 的Math.floor和 JavaScript 语义完全一致按标准写法来不会出错。第二个坑是数据的同步更新。自动填充时每个鸽巢的count都要同步修改不能只更新鸽子的nestId否则界面上鸽子是移动了位置但鸽巢的计数没有变显示就会错乱。正确的做法是在遍历鸽子分配巢号的同时直接更新对应鸽巢的count。由于两只鸽子的分配过程互不影响这个逻辑不需要加锁或者事务处理。实际写出来的分配函数核心代码类似这样assignRandomly() { const nestCount this.nests.length; let newNests this.nests.map(nest ({ ...nest, count: 0 })); let newPigeons this.pigeons.map(pigeon { const targetNest Math.floor(Math.random() * nestCount); newNests[targetNest].count 1; return { ...pigeon, nestId: targetNest }; }); this.nests newNests; this.pigeons newPigeons; }这段代码有两个关键细节值得说明。第一newNests里的count先重置为 0这样才能保证每次自动填充都从空巢开始否则上一次的累计值会叠加到这一次结果完全错误。第二所有修改都是在新的数组上完成最后才整体赋给状态变量确保 UI 百分百刷新。4. 实操细节与关键代码拆解4.1 手动分配模式的实现思路手动分配模式的交互逻辑是每一只鸽子图标上方都有一个“分配”按钮用户点击这个按钮后鸽子就会被分配到下一个鸽巢从巢 1 开始循环。这个模式的初衷是让用户自己控制分配过程从而更直观地理解“鸽子如何决定性地挤进同一个巢”。手动分配的状态记录是一个数组nextAssignIndex记录每一只鸽子下一次将被分配到的巢编号。初始时所有鸽子的nextAssignIndex都是 0分配后变为 1塞满一轮后回到 0形成环形分配。这个环形分配方式在实际操作中有一个好处用户按顺序点击分配时鸽子们会均匀分布只有在鸽子数量大于鸽巢数量时才会出现“某个巢有两只鸽子”的情况正好对应鸽巢原理的思想。手动分配还有一个隐含的问题需要处理如果鸽子的数量小于等于鸽巢的数量手动分配过程中可能永远不会有两只鸽子出现在同一个巢里这时候用户可能会觉得“鸽巢原理没有生效”。这是正常的因为原理本身要求 m n 才成立。为了让用户有完整的学习体验我在界面上加了一行提示文案实时显示当前的分配状态如果当前鸽子数量大于鸽巢数量提示“根据鸽巢原理一定存在至少一个巢有 2 只或更多鸽子”否则提示“鸽子数不大于巢数暂无必然重叠”。这个动态提示虽然代码量不大但极大提升了应用的教学价值。4.2 鸽巢卡片组件的封装与复用为了保持代码整洁我把鸽巢卡片单独封装成一个子组件。这既是 ArkTS 开发的最佳实践也是代码复用价值的体现。子组件接收父组件传递的鸽巢数据和鸽子列表内部通过Column和ForEach渲染鸽子图标。封装组件时需要注意一个重要的通信机制ArkTS 中父子组件数据通信通过Prop或ObjectLink装饰器实现。Prop适合传递基本类型或简单对象ObjectLink适合传递复杂对象且需要父组件同步更新的场景。我在实际项目里发现鸽巢卡片只需要展示数据不需要在子组件内部修改数据所以用Prop就够了。父组件修改数据后通过重新渲染自动把新值传给子组件子组件不需要写任何额外的更新逻辑。这个设计决策的核心价值在于保持单向数据流所有状态变化都通过父组件发起子组件只负责展示。在复杂的界面中单向数据流能大幅降低调试难度因为它把数据修改的位置集中在了一处。4.3 颜色反馈与视觉增强鸽巢卡片的颜色设计也是这个项目的一个亮点。初始状态所有鸽巢颜色为浅灰色。自动填充之后装有两只以上鸽子的鸽巢会变成浅红色装有一只鸽子的保持浅蓝色空巢保持浅灰色。这个颜色反馈直接告诉用户“哪几个巢挤了多只鸽子”是鸽巢原理结论的视觉化输出。实现方式是在NestCard卡片渲染时加一个条件判断count 1用红色count 1用蓝色count 0用灰色。不需要额外维护状态变量直接从鸽子数量推导出颜色。这也是声明式编程的一个好处视图状态可以通过计算得到而不是手动维护。颜色选择上有一些细节考虑。红色系我用的是#FF6B6B这种柔和一点的红色而不是纯红#FF0000因为纯红在浅色背景上容易显得刺眼。蓝色系用#4A9DFF灰色系用#E8E8E8。实测下来这种低饱和度的配色在平板和手机上显示都比较舒服也不会和文字颜色混淆。4.4 数据初始化与重置逻辑应用的页面加载时会执行一次初始化生成默认的鸽巢数据和鸽子数据。我设的默认值是鸽巢 5 个、鸽子 7 只一打开应用就满足鸽巢原理的条件用户不需要做任何操作就能看到效果。5 个巢和 7 只鸽子这个组合的选择是有讲究的它足够小所有卡片在一屏内能全部显示7 比 5 只多 2视觉上能明显看出至少有一个巢有多只鸽子但不会出现某巢挤太多导致卡片高度异常的问题。重置按钮的核心逻辑就是把所有状态恢复到初始值。这里有一个需要注意的点重置不只是把鸽巢数量恢复成 5鸽子的数量也要同步恢复成 7因为用户可能之前手动修改过鸽子数量。此外鸽子的nestId要重置为 -1鸽巢的count要重置为 0下次分配才能从头开始。遗漏任何一个字段都会导致界面显示残留数据。4.5 弹窗交互与结果展示当用户点击“自动填充”按钮后程序除了在界面上展示分配结果之外还会弹出一个结果面板。这个面板的内容是一段鸽巢原理的结论文字具体内容根据当前条件动态变化。如果满足鸽巢原理条件鸽子数大于巢数弹窗显示“当前有 7 只鸽子和 5 个鸽巢。根据鸽巢原理至少存在一个鸽巢中装有 2 只或更多鸽子。请观察上方的红色标记区域。”如果条件不满足弹窗则提示“当前鸽子数量不大于鸽巢数量鸽巢原理条件不成立。请增加鸽子数量或减少鸽巢数量后重新尝试。”弹窗用 ArkUI 的AlertDialog组件实现。需要注意的是弹窗的内容不仅限于文字也可以放自定义组件。我在弹窗里放了一个简化的统计表格统计当前每个鸽巢的鸽子分布方便用户对照。弹窗的实现过程也踩过一个坑AlertDialog确认按钮的回调事件如果涉及状态更新必须确保回调函数里this指向正确。在 ArkTS 中箭头函数能保留词法作用域的this而普通函数会在调用时重新绑定this指向调用对象而不是组件实例。我用的是箭头函数所以一切正常。如果误用了普通函数点击确认后界面不会刷新而且还不容易排查出原因。5. 常见问题与排查技巧实录5.1 输入为 0 时页面崩溃这个问题在功能测试早期频繁出现。用户把鸽巢数量或鸽子数量设置为 0点击自动填充程序直接崩溃。排查后发现两处问题第一处是生成鸽巢数组时循环条件i 0永远不成立数组为空后续对空数组的任何访问都可能报错第二处是分配鸽子时如果鸽子数量为 0不会有鸽子被分配但鸽巢的count也没重置显示上会出现残留数据。解决方案就是在输入校验阶段直接拦截这些非法值。现在程序的处理逻辑是只要检测到输入为 0 或负数立即弹窗提示“鸽巢数量和鸽子数量必须是大于 0 的整数”并且不执行后续逻辑。这不仅是防御性编程的要求也是对用户负责的表现——宁可提示错误也不要让程序在异常状态下静默运行。5.2 自动填充后界面不刷新这个问题是我在测试中发现的表现是数据状态已经变了鸽子的nestId已经更新但界面没有变化。排查之后定位到原因直接通过索引修改了State数组的元素属性导致 ArkUI 没有监听到变化。这在前面已经提到过State监听的是整个数组的赋值不是内部对象的属性变化。解决方式是统一用“创建新数组 - 修改新数组 - 整体赋值”的模式。这个模式看起来多写了一行代码但能保证 UI 的更新时机完全可控。另外补充一点如果使用Observed和ObjectLink装饰器也能实现对对象属性变化的监听但需要额外引入类定义对于这种简单场景有些过度设计。5.3 鸽巢数量变化时卡片布局错乱用户把鸽巢数量从 5 改成 30然后观察界面发现卡片挤成了一团或者出现了很多空白列。排查下来发现是Grid的列数模板没有动态更新。columnsTemplate属性只会在初始化时计算一次后续鸽巢数量改变不会自动重新计算列数导致布局错乱。解决方式是把columnsTemplate的计算逻辑放成一个函数在鸽巢数量变化后手动调用。这个函数根据当前屏幕宽度和鸽巢数量动态生成模板字符串。核心代码如下function gridColumns(): string { const count this.nests.length; if (count 6) return 1fr 1fr; if (count 12) return 1fr 1fr 1fr; return 1fr 1fr 1fr 1fr; }这个函数的计算逻辑是基于经验的鸽巢数量少的时候用 2 列保证卡片够大、看得清楚数量多的时候用 4 列虽然卡片小一点但能在更少的滚动范围内展示完全部鸽巢。实际测试下来这个策略在手机和平板上的表现都可接受。5.4 鸽巢计数和鸽子展示不一致这个问题的表现是界面显示某个鸽巢里有 3 只鸽子但鸽子图标只显示了 2 个或者显示 4 个。排查后发现原因是在更新状态时鸽巢的count和鸽子的nestId不是在同一次数据刷新里更新的。具体来说我一开始先更新了鸽巢的count然后才更新鸽子的nestId由于 ArkTS 的 UI 刷新是异步的界面可能在两个状态之间多次渲染就会出现短时间的计数和图标不一致。这个问题严格来说不会造成数据错误但视觉上确实影响体验。解决方式是保证鸽巢和鸽子在同一个事务里更新。用前面展示的“创建新数组 - 修改 - 整体赋值”模式把两个数组的赋值逻辑放在同一个函数体内就能保证它们在同一帧内更新避免中间状态的渲染。5.5 大数量输入导致界面卡顿当鸽巢数量设为 50、鸽子数量设为 100 时自动填充后界面会明显卡顿。主要原因是ForEach循环要渲染 50 个鸽巢卡片 100 个鸽子图标总共有 150 个节点。在 ArkUI 中节点数量超过 100 个后刷新性能会显著下降。解决方式有两个方向。第一个是限制最大值我在输入校验中把鸽巢数量限制在 50、鸽子数量限制在 100同时在自动填充时增加防抖处理快速连续点击按钮时忽略第二次操作。第二个方向是优化渲染比如鸽子图标用纯色圆点而不是复杂图片减少每个节点的渲染开销。两个方向我都做了实测下来卡顿感明显减轻。6. 扩展思路与实际体验总结这个鸽巢原理模拟器从开始构思到最终完成大概花了两天时间。第一天主要写界面和基础逻辑第二天做输入校验、颜色反馈、弹窗交互这些细节打磨。整体来说这个项目的难度不高但覆盖了 ArkTS 开发中相当一部分核心知识状态管理、数组操作、组件封装、条件渲染、弹窗交互、响应式布局。对于刚接触 HarmonyOS 开发的学习者来说是一个很好的综合练习项目。如果后续要扩展我有几个方向供参考。第一个方向是加入动画效果鸽子在移动时使用位移动画过渡视觉上会更生动。第二个方向是加入持久化存储把用户上次的输入参数保存下来下次打开应用自动恢复。第三个方向是改成多关卡模式每关设定具体的鸽巢数量和鸽子数量做成闯关形式适合放进教育类应用里作为课后互动练习。根据我个人实际操作下来的体会这种教学类小应用最大的价值不在于代码技巧有多高深而在于能不能把一个抽象的原理用最直观的方式展示出来。鸽巢原理本身一句话就能说清楚但用户亲手操作、亲眼看到“多出来的那只鸽子必须挤进某个已有鸽子的巢”这个过程理解深度完全不一样。这也是我做这个合集一直坚持的原则每个实例都要让人亲自动手试一遍而不是看完代码就完事。最后分享一个开发小技巧这个项目里所有涉及状态更新的操作我都统一封装成了方法不在 UI 回调里直接写业务逻辑。比如把“重置”“自动填充”“手动分配一步”都抽成独立函数界面上的按钮只负责调用这些函数。这个习惯在项目小的时候感觉不到好处但一旦界面复杂度上来调试和维护的效率差距会非常明显。