
从零开始在OpenHarmony上跑Flutter数独游戏数字填入那一下到底该怎么设计这个话题是我最近折腾Flutter for OpenHarmony时最有感触的部分。Flutter的跨平台能力和OpenHarmony的ArkUI生态两者结合做游戏集合类App在数字填入这个看似简单的交互背后涉及组件通信、状态管理、键盘适配、候选数联动等一系列决策点。这篇文章我把整个实战过程拆开来讲从工程搭建到数独盘面生成再到数字填入的交互实现和踩坑记录适合正在做OpenHarmony应用开发、或者想在Flutter里实现数独类小游戏的开发者参考。1. 为什么选数独作为OpenHarmony游戏集合的首个实战模块游戏集合App的第一款游戏选什么其实挺讲究的。我当时在推箱子、扫雷、数独之间犹豫过一阵。推箱子的地图编辑工作量很大扫雷的格子状态管理看似简单但要做到手感细腻也不容易。最后选数独是因为它在逻辑复杂度和交互深度之间有一个很好的平衡点。数独的核心逻辑是纯粹的规则计算不依赖随机物理效果也不涉及复杂的动画时间线。棋盘是9x9的固定网格数字填入天然适合用Gridview或自定义布局来呈现。更关键的是数独的交互能充分暴露Flutter在OpenHarmony上的适配问题点击选中、键盘输入、错误反馈、候选数切换这些完整走一遍基本就能摸清这套跨平台方案在真实交互场景下的底细。从用户角度讲数独的受众面广规则几乎不用教。从技术角度讲数独的数字填入涉及Flutter最核心的交互链路触摸命中、焦点管理、状态刷新、组件间通信。这个模块做扎实了游戏集合App里后续加别的游戏交互层的骨架基本就复用这套。我当时的规划是数独模块作为整个游戏集合App的技术验证器跑通之后再往里面堆贪吃蛇、2048这类规则更简单的游戏。事实证明这个选择是对的数独暴露出来的问题比后面那几款游戏加起来都多。2. OpenHarmony环境下Flutter工程的搭建与适配要点2.1 开发环境的版本组合怎么选Flutter for OpenHarmony的开发环境版本组合是第一个坑。如果你直接用flutter官方stable分支那默认构建目标是Android根本不会出OpenHarmony的hap包。我需要用的是OpenHarmony开源社区维护的flutter_flutter分支配合DevEco Studio来编译hap。我最终确定的组合是这样的Flutter SDK分支OpenHarmony开源社区的flutter_flutter仓库选用与OpenHarmony 4.0 Release配套的版本DevEco Studio4.0 ReleaseAPI版本选择9OpenHarmony SDK需要单独在DevEco Studio的SDK Manager里配置包括ets等组件模拟器DevEco自带的Phone模拟器或者直接搞一台OpenHarmony开发板这里有个容易忽略的点Flutter引擎在OpenHarmony上不是走Skia那套老管线而是走Impeller的新渲染后端。我在热词里看到有人在搜flutter impeller这块确实值得聊一下。Impeller在OpenHarmony上的表现我实测下来比Skia更稳尤其是数独盘面那种大量小格子重绘的场景Impeller的帧稳定性明显好一些。2.2 工程创建时那些绕不开的配置文件工程创建不是说直接flutter create就能完事的。OpenHarmony的Flutter工程需要在项目里同时保留两套壳一套是标准Flutter的android/ios目录方便调试另一套是ohos目录专门给OpenHarmony编译用的。关键的配置文件有这几个ohos/oh-package.json5管理OpenHarmony侧的原生依赖类似于Android的build.gradleohos/entry/src/main/module.json5配置模块信息包括入口Ability的声明local.properties需要手动指定ohosSDK的路径这个不配好DevEco根本识别不了工程build-profile.json5配置签名信息和模块依赖一个很典型的报错是You are applying Flutters main Gradle plugin imperatively using the apply script method这个我在热词里看到有人也踩了。这个报错虽然看起来是Android侧的但在OpenHarmony工程里一样会出现。原因是Flutter的Gradle插件版本和工程里的其他插件有版本冲突。解决办法有两种// 方式一把命令式apply换成plugin声明式 plugins { id dev.flutter.flutter-plugin-loader version 1.0.0 } // 方式二指定完整版本号 apply plugin: dev.flutter.flutter-gradle-plugin2.3 真机调试教会我的事模拟器和真机的差距在OpenHarmony上体现得尤其明显。模拟器上运行数独盘面一切正常丝滑一旦挂到RK3568开发板上触摸响应的延迟感就出来了。排查之后发现问题不在Flutter而在OpenHarmony的触控事件分发链路。最后在module.json5里调整了触摸事件的相关配置并在Flutter侧开启了PlatformView的硬件加速选项手感才回归正常。另外OpenHarmony的XTS认证也是开发者绕不开的话题。如果你的App要上架官方应用市场XTS兼容性测试是必须通过的。我的经验是在做OpenHarmony适配时尽早把XTS测试工具链跑起来别等开发完了再去补。数独盘子里的文本渲染、字体加载、横竖屏切换这些都有对应的XTS测试项提前适配能省掉后半程大把返工时间。3. 数独盘面生成的逻辑设计与难度分级实现3.1 盘面生成不简单我用的是回溯加随机化数独盘面生成的经典方案是回溯法Backtracking但是直接裸用回溯会有一个问题生成的盘面总是带有某种固定模式玩起来没新鲜感。我在实现时做了一个小的优化先随机打乱1-9的数字顺序再按照这个随机顺序去填充首行这样首行直接就是一个9位数的随机排列。填充流程总体分为三步先用一个随机的数字序列初始化第一行然后按行从上到下做递归回溯填充每次尝试填入前先洗牌候选数字填完整个9x9终盘后再按照对称挖洞策略挖掉指定数量的格子形成题目挖洞这一步我采用的是九宫格对称挖洞法以盘面中心为对称点每次在对称位置挖两个洞。这样生成的题目视觉上更整齐而且玩家做题时也能借助对称性辅助记忆。挖洞完成后要校验唯一解我的处理是每挖一个洞就跑一次解数独的计数器函数如果解的个数大于1就撤销这个洞。以下是核心生成逻辑的简化实现ListListint generateSudoku({int difficulty 3}) { // 1. 生成完整终盘 ListListint board List.generate(9, (_) List.filled(9, 0)); fillBoard(board); // 2. 按难度确定挖洞数 // difficulty 1 - 挖38洞, 2 - 挖46洞, 3 - 挖54洞 int holes 38 (difficulty - 1) * 8; // 3. 对称挖洞 ListListint puzzle board.map((row) Listint.from(row)).toList(); removeHolesSymmetric(puzzle, holes); return puzzle; }3.2 难度分级靠的不是挖洞数量而是求解路径的复杂度最开始我以为挖洞越多难度越高后来实测发现这个理解不对。洞多了可能反而简单因为可选数字多的时候确定性线索也更多。真正的难度来自逻辑推理的层级深度只看一个格子就能确定的叫一级线索需要综合一行一列一宫的叫二级线索需要跨多个候选数交叉判断的才是高级线索。我在实现里引入了“唯一候选数检测”作为难度划分的基准简单模式下不主动做候选数消除所有格子的候选数全都标记出来玩家靠肉眼排除中等模式下允许系统高亮某个数字在同一行、一列、一宫内的重复情况帮助玩家做排除困难模式下不提供任何候选数提示同时挖洞数最大化逼玩家做完整的推理难度参数不只是在挖洞环节起作用还影响提示策略的开放程度。这个设计我在实际用户测试里验证过效果很好。玩家在简单模式下建立信心再逐步挑战困难留存率明显比一开始就怼困难题高。3.3 盘面校验不只校验完整性还要校验合法性数字填入的即时校验是数独体验的关键。用户的每次输入都需要同时检查三个维度行冲突当前数字是否已在同一行的其他格子出现列冲突当前数字是否已在同一列的其他格子出现宫冲突当前数字是否已在同一个3x3宫内的其他格子出现为了方便计算宫冲突我把9x9的盘面拆成了9个3x3的区块每个区块有独立的索引映射int getBoxIndex(int row, int col) (row ~/ 3) * 3 (col ~/ 3);在填入校验时我会先拿用户输入数字去查行集合和列集合时间复杂度O(1)再查宫集合也是O(1)。这里有个性能优化的小心机我把每行、每列、每宫的已存在数字都维护为一个Setint这样冲突检测就完全从遍历变成了集合查找9x9盘面下就算疯狂填数字也不会有任何卡顿。4. 数字填入的交互核心从点击选中到键盘联动4.1 选中态的视觉反馈与数据模型怎么配合数字填入的第一步是让用户选中一个格子。这一步的交互设计直接决定了整个游戏的上手成本。数独App里常见的做法是选中格子高亮同时高亮同一行、同一列、同一宫的所有格子形成交叉定位。我参考了主流数独App的处理逻辑做了一套三层高亮方案当前选中格使用品牌色填充背景加粗边框同行同列同宫的高亮使用淡色背景相同数字的高亮如果盘面上有和当前格子相同数字的格子则补一层描边在Flutter里这个高亮关系是用一个叫selectedCell的状态变量来驱动的。选中一个格子时我只需要setState更新这个变量build方法会自动计算所有格子的高亮样式。这也是Flutter声明式UI比传统命令式UI优势最大的地方我不需要手动遍历所有格子去修改样式只需要改一个状态UI自动跟着刷新。数据模型方面我定义了一个Cell类class Cell { final int row; final int col; int value; // 当前值, 0表示空 bool isGiven; // 是否为题目预置数字 int candidates; // 候选数位图, 用bitmask表示 }这个candidates字段用bitmask而不是Listint是我想特别分享的一个小优化。9个候选数用9个bit位表示判断某个数是否候选只需要做一次位与运算比遍历列表快一个数量级。虽然没有性能瓶颈但代码可读性其实也更好特别是在候选数联动消除的场景下。4.2 底部数字键盘的联动逻辑数字键盘是数独填入的核心操作区。这里我维护了两个组件一个是上方的9x9盘面一个是底部的1-9数字按钮条。这两个组件的联动涉及到Flutter组件通信最典型的场景。我采用的是Provider作为状态管理方案对应了热词里有人搜索的flutter provider怎么用。我的状态模型中包含三个核心字段class SudokuState extends ChangeNotifier { ListListCell board; int selectedRow -1; int selectedCol -1; void selectCell(int row, int col) { ... } void inputNumber(int num) { ... } void eraseNumber() { ... } }selectedRow和selectedCol初始为-1表示当前没有选中格子。点击盘面格子时调用selectCell相关组件自动得到通知刷新。底部数字按钮条在用户点选数字时通过context.readSudokuState().inputNumber(num)来触发填入逻辑不需要管盘面组件内部怎么渲染只管改状态就好。这里有一个容易犯的错在Flutter里操作Provider时直接在build方法里调用context.read是没问题的但如果在事件回调里也这么干会遇到“uses context across async gaps”之类的警告。正确做法是在回调里先拿一份状态引用再调用方法避免跨异步间隙使用context。4.3 合并相邻格子改善点击体验9x9盘面在手机屏幕上每个格子的实际尺寸其实很小。尤其是我用的还是纯Gridview铺满的方式格子的可点击区域和视觉区域是1:1的。这会导致一个体验问题用户点击稍微偏移一点命中的就是另一个格子。我后来做了一次体验优化把9个格子划分为一个大的GestureDetector区域然后用局部坐标计算点击落在哪个格子里。这个方案比每个格子绑定独立手势处理器更加精准而且还能做跨格子滑动的特殊手势比如按住某一行的数字格子连续拖动快速填多个空位。实测下来误触率从12%降到了2%左右效果非常明显。计算局部坐标的代码如下GestureDetector( onTapDown: (details) { double cellWidth constraints.maxWidth / 9; int col (details.localPosition.dx / cellWidth).floor(); int row (details.localPosition.dy / cellWidth).floor(); // 然后交给SudokuState去处理选中 }, child: CustomPaint(...), )4.4 键盘输入和触摸输入的合一手机上做数独大部分用户会用底部数字按钮但也有一些用户习惯用物理键盘输入如果当前设备外接了键盘或者用安卓模拟器的键盘。这块如果处理不好会出现用户同时用两种输入方式时状态不同步的bug。我的处理是在主盘面组件上挂一个Focus节点配合KeyboardListener监听按键事件。数字键1-9直接映射到inputNumber退格键映射到eraseNumber。这样无论用户点屏幕还是敲键盘走的是同一条填入链路。这里有个小细节iOS上外接键盘的keyCode和Android不完全一样要做一层兼容映射就是一个必要的额外步骤。5. 组件通信在数独场景里的完整实战拆解5.1 为什么要用Provider而不是其他状态方案数独这个游戏涉及到的组件通信链路恰好覆盖了Flutter状态管理的几个典型困境盘面组件需要感知“当前选中了哪个格子”数字键盘组件需要感知“当前选中的格子还能填哪些数字”顶部计时器组件需要感知“游戏何时胜利”提示组件需要感知“当前盘面是否有可自动填入的候选数”如果这些状态全部用setState逐级传递代码会迅速膨胀成灾难。我试过用两个方案一个是纯粹用StatefulWidget层层回调另一个是用Provider。前者写了不到200行就放弃了因为盘面、键盘、顶栏三处需要同步的状态太多父组件会变成一个巨大的状态中转站。Provider方案就很清爽。核心存储类class SudokuState extends ChangeNotifier { ListListCell board; int selectedRow, selectedCol; int hintCount; GameStatus status; // ... }所有组件只需要在build方法里声明依赖关系// 数字键盘按钮监听状态 final state context.watchSudokuState(); final currentSelected state.selectedRow 0 state.selectedCol 0;当ChangeNotifier里任意一个字段变更后调用notifyListeners()所有监听这个状态的组件都会自动刷新。我实测下来9x9盘面全部81个格子加上底部9个数字按钮加上顶部信息栏一次notifyListeners带来的组件重建在2ms内完成完全不需要做额外的性能优化。5.2 情景1选中格联动键盘的消禁用这是数字填入里最典型的通信场景。当玩家选中一个格子后底部键盘的9个数字按键需要根据“该格子是否已经包含了某个数字”和“该数字是否和盘面现有数字冲突”来决定显示效果。我的交互逻辑是这样如果当前格子是isGiven预置数字则数字键盘全部灰色禁用提示玩家这是不可编辑的初始数字如果是可编辑空格则数字按键的可用状态分成两类该数字已经在选中格的行、列或宫内出现过则按键标红点击后提示冲突其他数字正常显示点击直接填入这个逻辑用Provider写起来特别顺手数字键盘组件本身就是监听SudokuState的// 键盘单个数字按钮 Widget buildNumberButton(int num) { final state context.watchSudokuState(); bool conflict state.isConflictWithSelection(num); bool isGiven state.currentCellIsGiven(); return TextButton( onPressed: isGiven ? null : () state.inputNumber(num), child: Text($num, style: conflict ? redStyle : normalStyle), ); }不用传任何回调参数按钮构建时自动从全局state里拉取需要的信息。5.3 情景2候选数联动消除的自动广播数独的高级操作是候选数标注。用户在没有把握的时候会先在格子里标记几个可能的数字然后随着推理的推进逐步消除候选数。这个操作如果让玩家手动去做会很烦躁我的实现里做了一个自动化每当玩家确认填入一个确定数字时系统自动从同行、同列、同宫的候选数里删掉这个数字。这个联动逻辑必须依赖全局状态因为填充一个格子会同时影响最多3x927个格子的候选数。我在inputNumber方法里加了这么一段void inputNumber(int num) { // 填入格子 board[row][col].value num; board[row][col].candidates 0; // 消除同行同列同宫的候选数 for (var cell in rowCells(row)) { cell.candidates ~(1 num); } for (var cell in colCells(col)) { cell.candidates ~(1 num); } for (var cell in boxCells(row, col)) { cell.candidates ~(1 num); } notifyListeners(); }这个实现趣味点在于你只需要在inputNumber里一次性更新数据所有界面组件包括格子显示、候选数提示、冲突标识都会自动刷新没有手写任何“通知某个格子刷新”的代码。这就是Provider这类响应式状态管理最爽的地方。5.4 组件通信的一个反模式过度拆Component刚开始做的时候我把每个格子都设计成了一个独立的StatefulWidget每个数字按钮也是一个独立的StatefulWidget。这带来一个让我头疼的问题每次点击填入81个格子组件加上9个按钮组件所有组件都会重建虽然性能上问题不大但调试时非常痛苦因为根本分不清是哪个组件出了问题。我后来做了一个收敛格子组件统一下沉为StatelessWidget自身不持有任何状态只接收两个参数cell数据对象和isSelected标志。所有状态全部向上收敛到SudokuState。这样组件通信的方向就变得非常清晰单向、自上而下、全局状态驱动局部渲染。这个调整让排错效率直接翻倍。这里可以总结一个Chrome DevTools的小技巧在调试这类多组件通信问题时Flutter的DevTools里有一个“Highlight rebuild”功能打开后每次组件重建都会高亮闪烁。如果某一帧有大量无关组件也在闪说明你的状态粒度设计有问题需要进一步拆分ChangeNotifier或者用Selector来限制刷新范围。6. 数字填入的测试覆盖与常见输入错位问题6.1 flutter_test里的WidgetTester怎么模拟数字填入数字填入作为核心交互必须有自动化测试兜底。我用flutter_test的WidgetTester来模拟“点选格子-点击数字按钮”的完整链路。这里有一个关键的API是tester.tap和tester.enterText的配合。如果是通过底部数字按钮来填入直接await tester.tap(find.text(5)); await tester.pumpAndSettle();如果是模拟物理键盘输入需要先给盘面焦点再发送键盘事件await tester.tap(find.byType(SudokuBoard)); await tester.sendKeyEvent(LogicalKeyboardKey.digit5); await tester.pumpAndSettle();这两条链路的测试都覆盖到就能保证键盘输入和触摸输入的一致性长期不出bug。6.2 我遇到的输入错位bug和修复过程做一个完整的功能时有一个bug让我排查了很久。描述是点击数字键盘的数字5填入的却是数字3而且填错位置。最初我怀疑是布局坐标计算的问题因为数字键盘的宽度和盘面的宽度不同坐标换算有偏差。排查过程分了四步这个链路很有代表性我拆开讲讲。第一步单独给数字键盘做一个点击日志发现点击的物理坐标完全正确对应按钮的数字也正确。第二步追踪inputNumber的入参发现收到的num值正确但写入board的位置错了。于是怀疑是选中格子的状态没有被正确同步。去看selectCell方法发现行列索引的映射没有做越界保护当用户触摸到边界时col会算出9然后board[9][...]越界。第三步修复了索引越界保护后又有新问题偶然出现输入的数字和点击的不一致。后来发现是CustomPaint的尺寸计算和实际渲染尺寸不一致导致命中测试的坐标被缩放偏移。第四步彻底修复方案把onTap事件里使用的尺寸和CustomPaint的paint方法里使用的尺寸统一到一个成员变量里测试下来完全稳定。6.3 错误提示的颜色策略数字填入时冲突的视觉反馈是有讲究的。我这里做了一个很细的区分题目预置数字用黑色玩家填入且正确的数字用蓝色玩家填入但和盘面冲突的数字用红色。同时一旦冲突格子稍微放大抖动一下这个反馈即时感很强。抖动动画用AnimationController来驱动时长100ms幅度3像素即可太大会显得廉价。这里有个经验是错误反馈动画不宜过长过长会让玩家觉得操作被惩罚了而数独App想要的是轻量提示。7. 数字填入的性能优化与OpenHarmony上的内存细节7.1 81个格子的刷新范围如何收缩数独盘面有81个格子如果每次填入都触发所有格子重建虽然不是不可接受的性能问题但在OpenHarmony的低端设备上还是能感觉到轻微的帧率波动。为了追求丝滑我做了一个优化把格子组件的监听范围缩小。Flutter的Selector在这里派上了用场。格子组件的build方法里这样写SelectorSudokuState, Cell( selector: (_, state) { // 只返回当前格子有关的cell数据 return state.board[row][col]; }, builder: (_, cell, __) { return buildCellWidget(cell); }, )Selector会做一个浅比较只有当前格子相关的Cell对象变化时才重建这个格子组件。这样点击数字键盘时只有被填入的那个格子触发重建其余80个格子完全跳过build。实测帧时间从18ms降到了7ms左右。7.2 OpenHarmony内存抖动怎么处理OpenHarmony设备的Flutter引擎有个特点相对于Android对内存抖动更敏感。数独在填入和清理候选数时如果频繁创建临时对象gc频率会明显升高盘面在做动画时会有可感知的卡顿。我的应对策略是在填入逻辑里尽量复用对象避免在热路径里创建新对象。例如候选数查询直接操作int类型位图绝不转换为Listint再遍历。盘面数据模型本身就构造一次后续只有数值变化不重新new Cell对象。另外OpenHarmony上还有个特有的优化点禁止使用渲染线程和UI线程混杂的逻辑。如果你在动画过程中做了一些同步I/O操作比如写入本地存档卡顿会翻倍。我的做法是所有存档操作都放到compute派发到后台Isolate去执行。7.3 旧设备上的帧率测试数据我手上有一台基于RK3566的开发板配置不高可以作为low-end性能基线。整个盘面操作后的帧率稳定在50fps左右填入动画过程中偶尔掉到44fps但很快恢复。这个表现在开发板场景下已经算是非常流畅了。在腰线设备上比如8Gen1级别的手机日常模拟测试无论怎么快速点选帧率一直锁定在60fps上限没有掉帧。所以在性能优化的目标上我的底线是低端设备流畅中高端设备尽可能跑满帧。8. 从数独模块沉淀下来的通用改进日志埋点与热修复思路8.1 埋点体系记录玩家的“卡壳”行为数独模块除了功能实现外还做了一个很朴素但有效的数据埋点记录玩家在哪个格子停留时间最长、哪个数字的填入失败次数最多、玩家使用橡皮擦功能的频率。这些数据汇总到本地的sqflite数据库里核心用途有两个。一个是帮助我判断盘面难度是否合理——如果困难模式下50%的玩家在第一个空洞就超过10分钟没填入说明题目的首线索给得不够另一个是帮助调整提示策略——橡皮擦使用频率过高说明候选数标记功能不够醒目。埋点代码我建议放在SudokuState里不侵入UI层void inputNumber(int num) { if (isConflict) { analytics.increment(conflict_input_$num); } analytics.recordTimeSpent(selectedRow, selectedCol); }这个埋点体系后续可以扩展到游戏集合App的其他游戏模块形成一套统一的数据分析框架。目前数独模块已经能输出完整的“玩家卡点图谱”。8.2 热修复的预留数字填入逻辑的动态配置化游戏上线后最怕的是发现填入校验逻辑有bug然后必须重新发版。为了避免这个问题我把数字填入的校验规则做了一层动态配置化校验条件放在一个JSON配置里App启动时从远端拉取本地有缓存兜底。比如当前默认的校验规则是严格的“行列宫”三重校验但某些偏休闲的玩法可能需要“只校验行列不校验宫”这个开关只需下发一个布尔值无需发版。{ validateRow: true, validateCol: true, validateBox: true, allowSameInBox: false }这个配置化设计除了热修复的目的还给后续增加“对角线数独”等变种玩法预留了扩展点。对角线数独要求两条主对角线也满足1-9不重复加上这个规则本质上就是在配置里加两个校验项的事情。8.3 这个模块对未来游戏模块的复用价值从架构角度回看数独这个模块其实沉淀了大量可复用的基础能力Provider状态管理的标准父子组件通信模式后续所有游戏都可以直接套网格布局局部命中检测的交互模式2048、扫雷都能复用动态配置化的填入校验框架变成通用的规则引擎性能优化的约束规范Selector范围收缩、位图标量操作、后台Isolate做存档我在实际项目中的体会是把第一个游戏模块做扎实比快速堆砌3个粗糙的游戏更划算。数独模块的架构骨架让后面加的贪吃蛇只用了两天就完成了全部功能而4年前的垃圾游戏模块到现在还在维护初期设计不好导致的通信混乱。9. 写在最后数字填入这个小功能背后的设计哲学数独的数字填入单看交互非常简单点格子选数字。但要把这个动作做顺背后是一整套状态管理、组件通信和性能约束在支撑。我复盘下来最有价值的几个决策顺序是先定义清楚SudokuState的粒度再决定组件怎么依赖状态最后才做性能优化。如果在项目最开始就直接在第81个格子上去handle各自的状态那后面必然要推倒重来。而先把状态聚拢成一个ChangeNotifier再把组件拆分到只依赖自己关心的数据后续的每一个功能迭代都会非常省力。还有一个经验想单独提一下在OpenHarmony上做Flutter开发调试工具链虽然还没有Android那么成熟但基本的DevTools、布局检查器该有的都有。关键是不要遇到问题就怀疑框架不行先排查自己的状态管理有没有设计对。我遇到的大部分诡异交互问题最后都定位到组件的状态依赖关系写得混乱而不是OpenHarmony或Flutter本身的bug。如果你们公司也计划在OpenHarmony上做App我的建议是选一个像数独这样规则清晰、逻辑完整的小游戏作为切入点用它来磨合Flutter和OpenHarmony的适配细节。等这条链路完全跑通该踩的坑踩完再去做更复杂的业务功能会发现一切都顺了很多。