
去年年底我做了一个决定把数独游戏从双端原生重构成 Flutter并且要求同一套代码能跑在 Android、iOS 和搭载 OpenHarmony 的设备上。这个项目里最让我花心思的不是盘面生成算法也不是错误检测而是那个看起来不起眼的“笔记功能”——英文社区叫 pencil marks就是在每个空格里用小字记录候选数字。你会以为这玩意儿不就是画几个小数字嘛真做起来才知道它牵涉到数据结构设计、UI 重绘策略、跨端组件通信甚至还有 Flutter 引擎在 OpenHarmony 上的渲染兼容性。这篇文章我把整个实战过程拆开讲清楚从选型到踩坑从原生适配到性能优化适合那些正准备把 Flutter 工程迁移到 OpenHarmony、或者在做数独类交互 App 的人参考。1. 为什么我说 Flutter 是 OpenHarmony 上做数独 App 的最短路径1.1 数独 App 的技术选型困局先交代一下背景。我当时手里有一个已经上线的数独 App功能包含每日挑战、计时模式、错误高亮、笔记功能线上用户不算多但评价很稳。那会儿团队在评估要不要适配 OpenHarmony 生态我面临一个很现实的选择用 ArkTS 把整个应用重写一遍还是用跨端框架一套代码多端跑。重写的问题很明显。数独游戏的核心逻辑跟 UI 耦合很深尤其是笔记功能盘面上 81 个格子每个格子都要维护候选数字集合再加上撤销栈、错误计数、提示系统重写一遍的测试成本非常高。而且团队里没人写过 ArkTS学习成本又是一个隐性支出。跨端方案里当时能稳定支持 OpenHarmony 的只有 Flutter 的 ohos 分支。React Native 的适配进度当时还不成熟uni-app 在复杂交互上的表现也一般。相比之下Flutter 的渲染引擎是自己实现的不依赖系统 WebView在 OpenHarmony 这种新系统上反而更容易做到底层兼容。所以最终我定了主工程保留 Flutter通过 ohos 分支构建出 HAP 包跑在 OpenHarmony 设备上。1.2 Flutter for OpenHarmony 的适配现状这里先给没接触过的人补个背景。Flutter 官方主分支是不直接支持 OpenHarmony 的需要拉取 OpenHarmony SIG 维护的 flutter_flutter 仓库切到 ohos 分支用配套的 Flutter 引擎和工具链来构建。整个工程结构跟标准 Flutter 工程有差异最明显的是多了一个 ohos 目录它承担了类似 Android 工程里 android 目录的角色。构建产物也不再是 APK而是 HAP 包。命令从 flutter build apk 变成了 flutter build hap打包完成后会生成 entry 模块的 HAP可以直接用 DevEco Studio 的签名工具签名或者通过 hdc 命令行安装到 OpenHarmony 设备上。关于环境搭建我建议按这个顺序来1.安装 DevEco Studio并且通过 SDK Manager 装好 OpenHarmony SDK同时记下 SDK 路径后面配环境变量要用。2.拉取 flutter_flutter 的 ohos 分支比如git clone -b ohos https://gitee.com/openharmony-sig/flutter_flutter.git。3.把 ohos 分支的 Flutter 工具目录加入 PATH并且设置 DEVECO_SDK_HOME 指向 DevEco Studio 的 SDK 目录。4.在项目根目录执行flutter create --platforms ohos .生成 ohos 平台目录。5.执行flutter doctor检查环境看到 Flutter 和 OHOS toolchain 都正常就说明基础环境通了。我一开始在这上面栽过跟头主要原因是 DevEco Studio 自带的那套 JDK 和 Flutter 用的 JDK 版本不一致导致 ohos 工程无法构建。解决办法也不复杂统一用一个 JDK 17然后让 DEVECO_SDK_HOME 指向同一个 SDK 根目录就行。整体来说这套工具链现在是可以顺畅跑通的但前提是版本得对齐我用的是 Flutter 3.7.x 对应的 ohos 分支和 DevEco Studio 4.0 左右的版本稳定性会好很多。1.3 笔记功能为什么是检验适配深度最好的试金石你可能想问一个笔记功能而已为什么值得单独立项拿出来讲。这是因为笔记功能恰好踩中了跨端开发里所有比较痛的环节。第一它的数据模型复杂。81 个格子每个格子的候选数字集合需要频繁读写还涉及撤销、重做、错误检测这些联动逻辑。第二它的 UI 重绘频率高。用户每点一个数字按钮界面上至少有几个格子的笔记要更新如果设计不好就是整盘重绘在 OpenHarmony 的低端设备上会非常卡。第三它需要细腻的交互判断。用户什么时候是填数、什么时候是记笔记光靠按钮切换还不够还得处理长按、多选、快速连续点击这些边界情况。第四跨端适配的细节都藏在里面。同样的交互逻辑在 Android 上运行没问题到了 OpenHarmony 上可能因为组件通信、渲染引擎差异、触摸事件处理方式不同而出现各种诡异的问题。所以我后文讲的每一条思路基本都是从笔记功能这个项目里提炼出来的但方法论可以直接复制到你自己的 Flutter for OpenHarmony 项目里。2. 笔记的本质不只是 UI 上多几个小数字2.1 用位掩码还是 Set数据结构决定了后面所有代码的写法先说结论笔记数据用整型位掩码存不要用 Set。这个结论是我在重构过程中踩了坑之后才想明白的。第一版实现我用了Setint存候选数写起来确实直观比如Setint notes {1, 3, 5};但问题很快暴露出来了。第一个痛点Set 的内存占用大。81 个格子每个格子都持有一个 Set 实例再加上撤销栈里要存历史状态一次游戏下来会积攒大量对象触达 GC 时能明显感觉到掉帧。第二个痛点Set 的判断效率低。我需要经常判断某个数字是否在候选集合里、两个集合是否有交集、某一行所有格子的候选并集是什么这些操作用 Set 做都要走迭代器性能开销比位运算高一个数量级。第三个痛点序列化不方便。撤销栈要深度拷贝状态Set 拷贝起来又慢又容易踩坑。改成位掩码后一切变得清爽。规则是这样用一个整数变量 notes二进制位上从低到高依次代表数字 1 到 9第 n 位从 0 开始为 1 表示数字 n1 是候选数初始有一个很好的性质9 个数字最多用掉 9 个 bit一个 int 就装得下。比如数字 2 和 5 是候选就写成二进制 10010换算成十进制就是 18。对候选数的操作也全部改成位运算// 添加一个候选数 notenote 范围是 1-9 void addNote(int note) { notes | (1 (note - 1)); } // 移除候选数 void removeNote(int note) { notes ~(1 (note - 1)); } // 判断数字 note 是否在候选集合中 bool containsNote(int note) { return (notes (1 (note - 1))) ! 0; }这里有一个新手容易踩的问题位运算符号优先级。在 Dart 里和|的优先级低于所以notes 1 (note - 1) ! 0这种写法实际执行顺序会变成notes (1 ((note - 1) ! 0))编译倒是能通过结果完全不对。我的一贯做法是强制加括号宁多勿缺。数据模型上我把每个格子抽象成一个 Cell 对象整个盘面用一维 List 存而不是二维数组这样序列化和撤销栈操作会更方便class SudokuCell { int value; // 0 表示未填 bool isGiven; // 是否是初始题目格 int notes; // 笔记位掩码 SudokuCell({this.value 0, this.isGiven false, this.notes 0}); SudokuCell copy() { return SudokuCell(value: value, isGiven: isGiven, notes: notes); } }2.2 笔记与填数的联动规则笔记功能不是孤立存在的它必须跟填数、错误检测、撤销栈这些系统联动。常见的规则有四条每一条都影响数据设计第一条填入一个数字后该格子的笔记必须清空。这一点好理解但要注意清空时机。必须在修改 value 的同一个事务里完成否则会出现格子既有值又有笔记的脏状态。第二条在当前行、当前列、当前 3x3 宫格内所有其他格子的笔记中要移除刚填入的这个数字。假设你在 r2c4 这个格子填入了 7那么整个第 2 行、第 4 列、以及 r2c4 所在的九宫格里的所有空格笔记里的 7 都要被移除。这个联动是数独笔记的核心做题提示、候选数消除都靠它。第三条如果某个格子的笔记集合只剩一个数字这个格子就叫裸单Naked Single按理说它可以直接被填入但很多数独应用并不会自动填而是通过高亮提示用户。我保留手动确认的操作路径。第四条撤销操作要同时回滚 value 和 notes。这也是我把 Cell 设计成不可变对象、通过 copy 生成快照的原因撤销时直接恢复整个 Cell 状态比逐个字段回滚可靠得多。这些联动逻辑我一开始散落在各个方法里后来发现 bug 太多就抽成了一个 BoardService统一管理所有格子状态变更。每次变更都会把当前 List 的快照推入 undo 栈然后把变更后的盘面广播给上层 UI。2.3 一道题从笔记到填数的完整状态流转拿一个具体场景串一下开局后盘面上有初始题目某个空格 r1c2当前 value 为 0notes 的位掩码是 0表示这是个全新空格没有任何候选记录。用户此时开启了笔记模式然后点击底部数字键盘上的 4这个动作会进入 addNote 流程。BoardService 判断该格子当前 value 为 0于是把 notes 置为1 3数字 4 对应的位也就是二进制 01000。所有监听者在得到通知后会重绘 r1c2 这个格子的副层 UI显示一个小数字 4。接着用户退出笔记模式点击 4 填入。BoardService 会执行填入事务先把 value 从 0 改成 4把 notes 清 0然后遍历第 1 行、第 2 列、以及 r1c2 所在的宫把每个空格的笔记里第 4 位清掉。如果在清理过程中某个格子的笔记位数从 2 变成了 1那么它会在 UI 上高亮成一个候选提示提示用户这里可以填了。这个过程看起来不复杂但如果没有清晰的统一入口后续做错误高亮、提示系统、撤销功能时就会越改越乱最后变成一个谁都不敢碰的屎山。我最后把 BoardService 设计成了全局单例 ChangNotifier跟 Flutter Widget 树完全解耦后面接什么 UI 框架都无所谓。3. 手写九宫格 UI小数字布局、手势判定与重绘优化3.1 单元格内 9 个小数字的布局方案UI 上最直观的难点一个格子大概只有 40 到 50 像素宽这里面要塞下最多 9 个小数字还要让用户一眼认出哪些是候选、哪些是被排除的。我试过两种主流方案。第一种是单行省略号形式就是在一行里把候选数字按小字串列比如“1 2 4”。优点是实现简单缺点是一旦超过 4 个数字就显示不全在九宫格密排的场景下用户体验很差。第二种是 3x3 子网格每个格子里放一个 3x3 的 GridView把 9 个数字按三行三列排布。这也是多数成熟数独 App 采用的做法。我最终选了第二种但没直接嵌套 GridView而是用 LayoutBuilder 加 GridView.count注意要启用 shrinkWrap并且把 NeverScrollableScrollPhysics 设为 true否则外层盘面滚动时内层格子会抢手势Widget buildNotesLayer(int notes) { return LayoutBuilder( builder: (context, constraints) { final cellSize constraints.maxWidth / 3.0; return GridView.count( crossAxisCount: 3, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), children: List.generate(9, (index) { final num index 1; final isActive (notes (1 index)) ! 0; return isActive ? Center( child: Text( $num, style: TextStyle( fontSize: cellSize * 0.45, fontWeight: FontWeight.w500, color: noteActiveColor, ), ), ) : const SizedBox.shrink(); }), ); }, ); }这里有个性能陷阱。如果你直接在 81 个格子外层做 setState每次笔记更新81 个 GridView 会全部重建一局游戏玩到后期想不卡都难。我的做法是把这个 notesLayer 抽成独立的 StatelessWidget并且用 RepaintBoundary 包住只有对应格子的 notes 变化时才重绘。后文第 6 章我会专门讲重绘优化这里你先记住这个结论。3.2 笔记模式与填数模式的交互切换交互设计上最经典的模式切换方式是提供两个入口顶部工具栏的铅笔按钮和数字键盘上的“笔记”切换键。按下后进入笔记模式此时点击数字键盘上的数字行为从填入变为切换候选数字。但光有这个还不够。我在实测中发现用户在快速做题时频繁切换模式非常打断思路所以我在底部键盘上做了一块特殊的交互优化长按数字键临时进入笔记模式松手后回到填数模式。这个长按交互的判定阈值很讲究超过 250 毫秒算长按进入笔记模式不足 250 毫秒但大于 80 毫秒算点击执行填数。太短会被误判为点击太长又会让用户觉得卡顿。代码层面我对数字键加了一套 GestureDetector结合 onTapDown 启动计时器、onTapUp 取消计时器、onLongPress 触发笔记模式GestureDetector( onTapDown: (_) _startPressTimer(), onTapUp: (_) { _cancelPressTimer(); if (!_isLongPressTriggered) { onTapNumber(); } }, onLongPress: () { _isLongPressTriggered true; onLongPressNote(); }, child: _buildNumberKey(), )这里面有一个 Flutter 的手势竞技场问题。onTapDown 和 onLongPress 同时存在时长按触发后系统会自动取消 onTapUp 回调这是预期的但你自己的手写计时器要格外小心必须在 onLongPress 触发后看一眼当前状态避免计时器回调重复触发笔记操作。我加了一颗_isLongPressTriggered标志位实测下来这个 bug 就不再出现了。3.3 状态管理的选择为什么我用 ChangeNotifier 而不是 Stream笔记这个功能数据变化频率高、但变化范围局部性强理论上用 InheritedWidget 或者 Provider 这种基于 ChangeNotifier 的库就够了。我自己实现了一个纯 Dart 的 ChangeNotifier 子类 BoardService配合 Flutter 的 ValueListenableBuilder 绑定到具体格子整体维护成本很低。我试过 Stream 方案但踩了个坑。Stream 的流式传播天然适合数据管道、异步事件流但对这种高频小状态变化默认没有合并批量变更的机制。用户在快速清笔记时每点一个数字就会发一个事件到 Stream 里监听者如果每次都触发 setState性能反而不如 ChangeNotifier 手动批量通知好。这里有朋友会问为什么不直接上 Provider 或者 Riverpod原因是我的棋盘嵌套层级很浅81 个格子都在同一个 GridView 里用 Provider 的 context.watch 会导致整个盘面 rebuild我的优化思路是尽可能让每个格子独立监听减少重建范围。所以我退而求其次在格子组件里用 ListenableBuilder 直接监听 BoardService 的某一格字段只重建这一个格子组件。class SudokuCellWidget extends StatelessWidget { final int index; const SudokuCellWidget({super.key, required this.index}); override Widget build(BuildContext context) { return ListenableBuilder( listenable: BoardService.instance, builder: (context, _) { final cell BoardService.instance.cells[index]; return buildCell(cell); }, ); } }这个方案的另一个好处是跨端一致性强。BoardService 是完全纯 Dart 的实现不管你在 Android、iOS 还是 OpenHarmony 上跑行为完全一致这给跨端测试减少了很多心智负担。4. 把 Flutter 工程跑上 OpenHarmony环境、构建与组件通信4.1 环境搭建与工程对接如果说前面讲的都是纯 Flutter 逻辑那从这一章开始才是真正跟 OpenHarmony 系统打交道的部分。我的经验是环境搭好一半的坑已经避开了。我建议严格按照官方 ohos 分支文档操作但有几个点在文档里写得不够细。首先是 DevEco Studio 的 SDK 版本要和 Flutter 引擎期望的版本匹配。我记得当时用 flutter doctor 检查时系统提示需要某一版 API而我安装的是另一个版本导致生成的 include 路径对不上构建时报了一堆类似找不到头文件的错误。后来我把 SDK 和 Flutter 分支都对齐到对应 tag 才解决。其次是环境变量。OpenHarmony 构建链路需要同时依赖DEVECO_SDK_HOME指向 DevEco Studio 的 SDK 主目录JAVA_HOME建议用 DevEco Studio 自带的 JBR不要用系统里的其他 JDK版本不一致会直接编译失败PATH要把 Flutter 的 bin 目录放进去。这些配完之后建议先跑一遍flutter doctor看到 OHOS toolchain 正常再继续。否则后面报错会非常折磨因为你根本分不清是环境问题还是代码问题。4.2 组件通信和 PlatformView 的取舍Flutter 工程在 OpenHarmony 上运行本质上还是 Flutter 引擎进程和 OpenHarmony 系统层之间做桥接。如果项目里要调用系统能力比如应用生命周期、震动、相机这类就需要走 MethodChannel 或 EventChannel。这个机制在 OpenHarmony 上和 Android 上非常像但实现细节不同。Android 上是在 MainActivity 里注册 MethodChannel 的 HandlerOpenHarmony 上则是在你的 Ability 或 AbilityStage 里注册。代码结构上有差异但思路一致。我在项目里就用 MethodChannel 实现了震动反馈调用const methodChannel MethodChannel(sudoku_app/haptics); Futurevoid vibrate() async { try { await methodChannel.invokeMethod(vibrate, {durationMs: 30}); } catch (e) { // 低端设备或模拟器可能不支持静默失败 } }对应的 OpenHarmony 侧代码在 MainAbility 文件里通过onRegister或者类似生命周期里注册接收 method 调用后调用系统的 vibrator 模块。要注意的是凡事都要做好容错因为 OpenHarmony 的 API 在不同设备上实现程度不一样有的设备上根本没有震动马达。PlatformView 是另一个大坑。Flutter 里嵌入原生 View比如地图、视频播放器会用到 PlatformView在 OpenHarmony 上的适配成熟度现在还不太够。我的结论是能用 Flutter 自绘解决的就不要用 PlatformView。像数独这种 App几乎没有什么需要嵌入原生 View 的场景所以我全程避开了。如果你确实需要嵌入我建议优先尝试 Texture纹理方案也就是把原生内容渲染到共享纹理上比直接嵌入原生 View 的兼容性要好得多因为后者在 OpenHarmony 上涉及到窗口和 Surface 的分配很底层出问题很难排查。4.3 Impeller 与 Skia 在鸿蒙上的表现Flutter 渲染引擎这块是我在 OpenHarmony 适配过程中切身感受到差异最大的地方。Flutter 3.7 时期默认用的是 Skia 后端后来新版本逐渐切到 Impeller。Impeller 的优势是预编译着色器减少首次运行的卡顿理论上渲染更平滑。但是在 OpenHarmony 的 ohos 分支上Impeller 的支持还不够成熟。我实测发现在某些设备上开启 Impeller 后笔记快速切换时会有肉眼可见的闪烁个别设备甚至直接白屏。后来我看了日志发现是 Impeller 的 Vulkan 后端跟 OpenHarmony 的图形栈兼容性存在问题。我当时的处理办法是显式切回 Skia 渲染// 在 main.dart 里强制指定渲染器 void main() { if (Platform.isOpenHarmony) { // ohos 分支支持 --enable-software-rendering 或者 --no-enable-impeller } runApp(const SudokuApp()); }具体开关通过运行参数控制你可以直接在运行命令里加--no-enable-impeller或者在 pubspec 对应的配置文件里做环境区分。我的建议是在 OpenHarmony 真机验证之前先默认关闭 Impeller优先保证稳定。另一个渲染相关的问题是文字渲染。ArkUI 和 Flutter 的字体渲染栈不同在部分设备上Flutter 小字号文字会显得发虚、笔画不够锐利。数独笔记的数字本来就小这个问题会被放大。我最终通过自定义 TextStyle把字体加粗到 w600并适当调大字号来补偿清晰度。5. 踩坑排错从一行 unhandled 异常日志开始的排查链路5.1 崩溃日志信息怎么读聊到排错我先说一个所有 Flutter 开发者在 OpenHarmony 上都会遇到的日志问题。当你用 hdc 连接 OpenHarmony 设备拉日志时经常能看到这样的内容e/flutter (31173): [error:flutter/runtime/dart_vm_initializer.cc(41)] unhandled exception: ...这种日志是 Flutter 引擎把 Dart 层的未捕获异常转发到系统日志里的。我第一次看到时有点慌因为字符串里的文件名是 dart_vm_initializer.cc看起来像引擎源码错误实际上它只是入口真正有用的信息在后面几行。你需要往上下翻找类似这样的堆栈信息e/flutter (31173): #0 BoardService._clearLinkedNotes (package:sudoku_app/services/board_service.dart:123) e/flutter (31173): #1 BoardService.fillNumber (package:sudoku_app/services/board_service.dart:78) ...读日志的要点是定位 Dart 层堆栈而不是看那个 cc 文件名。我见过不少人在社区里问 flutter 引擎是不是挂了其实只是自己代码 Null 判断没做。5.2 Future.then 回调时序微任务队列带来的刷新问题这个坑我印象特别深因为它从代码逻辑角度很难察觉是异步时序问题。当时我做了一个自动记录笔记的功能用户填完一个数后异步调用一个算法模块自动计算当前盘面的候选数更新整个盘面的笔记。算法是放在 Isolate 里跑的通过 compute 执行计算完成后用 Future 回调更新 UI。理论上看起来没问题但实测中我发现有些格子的笔记更新频率比预期低而且偶尔出现旧数据覆盖新数据的现象。后来我查阅了 Flutter 的异步机制Future.then的回调是放入微任务队列的它会赶在下一帧渲染前执行完所有微任务。如果算法计算耗时较长回调里的 UI 更新会被排到很晚期间用户已经手动改了笔记我再用计算结果覆盖等于把用户的手动修改冲掉了。我换成了 Stream 节流的方式算法结果通过 Stream 发送UI 侧做防抖处理只有距离上次结果超过 200 毫秒才应用。这样既避免了回调时序问题又能批量应用算法结果一次刷新多个格子。5.3 下拉刷新与笔记清空的生命周期纠缠这个坑是典型的生命周期管理问题发生在练习模式的局列表页。我在列表页用到了 Flutter 的下拉刷新组件 RefreshIndicator列表项是用户最近玩的棋局支持长按删除。问题出在从列表页点进棋局后用户回到列表页触发下拉刷新结果发现正在编辑的棋局里的笔记被清空了。原因不复杂列表页保留了一个 BoardService 的全局实例下拉刷新会触发重新拉取棋局列表拉取成功后我没有对当前棋局的引用做保护导致全局实例被重置。而笔记数据是存在棋局对象里的重置实例等于丢掉了笔记。解法是我在棋局数据层引入了持久化存储每次笔记变化就写入本地数据库下拉刷新只刷新棋局元信息不会触碰棋盘状态。这个改动也顺带解决了 App 被杀后重启恢复棋局的问题算是一举两得。6. 性能优化、XTS 认证与后续扩展6.1 重绘优化从局部 setState 到 RepaintBoundary前面提到过格子的重绘问题这里我把完整的优化链路讲清楚你在其他 Flutter 项目里也能用得上。第一层优化不要让整个盘面 rebuild。把盘面拆成独立的 SudokuCellWidget每个格子只监听自己的 index 对应的 Cell。棋盘 81 个格子实际响应的只有变更的那几个。第二层优化用 RepaintBoundary 夹住每一个格子。RepaintBoundary 的作用是隔离重绘边界格子的笔记层变化不会触发父级重绘尤其是不会让棋盘九宫格的大边框跟着重绘。第三层优化笔记层与主值层分离。我把数值层和笔记层做成两层 Stack数值层用完全不透明背景笔记层是透明背景。这样数值变化时笔记层不受影响笔记变化时数值层不受影响进一步缩小重绘范围。实测效果在 OpenHarmony 真机上快速连续点击数字键盘帧率能稳住 55 到 60 帧棋盘整体没有明显的闪烁或者掉帧。而在优化前连点十几次就会开始卡顿一到真机上的低性能模式就原形毕露。6.2 上架前的 XTS 认证注意点如果你是要把 Flutter 应用上架到 OpenHarmony 的应用市场会遇到一个叫 XTS 认证的东西全称是 OpenHarmony Compatibility X Test Suite是一套兼容性测试。Flutter 应用因为是混合栈跟普通原生应用比有一些额外的注意点。我遇到的第一个问题是权限声明的合规性。Flutter 插件在编译时会自动生成访问系统能力的模板代码如果某些插件声明了敏感权限而实际没有用到XTS 测试会直接报警。当时我集成的一个震动插件默认声明了身体传感器权限数独游戏用不到认证检查就不通过。后来我手动删掉了插件里多余的权限声明。第二个问题是资源文件的规范。XTS 对应用图标、屏幕方向配置、版本号一致性这些都有硬性要求。Flutter 生成的默认配置需要手动调整到符合 OpenHarmony 的格式比如应用图标尺寸要符合标准不然兼容性测试会提示图标问题。第三个问题是混合栈稳定性。XTS 里有大量压力测试和稳定性测试如果 Flutter 引擎和 OpenHarmony 的桥接层不稳定在快速切换页面、长时间运行时可能会出现崩溃。这块就是我们前面说的 Impeller 和渲染后端的稳定性问题所以我的建议很简单上线前一定要在 OpenHarmony 真机上做至少连续 12 小时的压力测试用脚本自动点击棋盘、切页确保没有内存泄漏和崩溃这个比什么优化都实在。6.3 这个笔记功能还能怎么扩展笔记功能做完我顺手想了几个扩展方向这里一并分享出来第一个方向候选数高亮联动。当用户选中某个格子时同行、同列、同宫里所有包含该格子的候选数的其他格子都能高亮提示。这个功能对新手用户帮助很大实现也不算复杂只需要在选中格子变化时遍历范围格子判断 containsNote。第二个方向解题提示系统。基于笔记数据做排除法提示用户点“提示”按钮时算法找到那些裸单格子把唯一候选高亮出来引导用户自己填而不是直接给答案。这样既不破坏游戏可玩性又能提高用户留存。第三个方向输入法的完整支持。目前笔记模式只支持点击数字键后续可以考虑支持手势滑入比如从数字键滑到某个格子上自动填充笔记。我在 OpenHarmony 上实测过 Flutter 的手势滑动跟数字键盘的组合还比较流畅这个交互值得做。第四个方向自动笔记重算。这就是我前面提到的 compute 加 Stream 的方案可以做成一个开关用户可以选择打开“自动候选数更新”让系统在每步操作后自动重算笔记省去手动画笔记的过程。不过要提醒一点自动更新会把玩家手动记的笔记清掉所以必须在设置里给用户足够的说明。这几个方向任意挑一个都能让数独 App 的笔记功能从“能记数”变成“教学向助手”这也是我接下来准备继续做下去的方向。说实话跨端适配这种工作做的时候痛做完之后回头看会发现对理解 Flutter 引擎和系统底层有着非常大的帮助不怕踩坑就怕踩完坑不留记录写这篇文章就当是给当时的自己留一份完整的踩坑笔记吧。