OpenHarmony生态里跑Flutter,听起来是件有点折腾的事,但我还是拿一个成语接龙小游戏试了一把。用Flutter for OpenHarmony做文字游戏,最大的好处是UI逻辑可以复用,一套Dart代码既能跑Android/iOS,也能在国产系统上编译运行。成语接龙这种纯交互、弱硬件的项目,刚好适合验证Flutter跨端能力在OpenHarmony上的落地程度。这篇文章就把我整个落地过程写出来,从环境搭建、核心规则实现,到网络请求封装、内存优化,最后附上踩坑记录,适合正在研究Flutter跨端、或者想在OpenHarmony上练手的开发者参考。
1. 项目整体设计与思路拆解
1.1 为什么选 Flutter for OpenHarmony 来做文字游戏
提到OpenHarmony应用开发,很多人第一反应是用ArkUI和ArkTS,但我的场景比较特殊:团队原本就有一套基于Flutter的业务代码,里面封装了请求库、动画组件、词库解析逻辑,如果全部用ArkUI重写一遍,成本非常高。Flutter for OpenHarmony的意义就在于,它把Flutter Engine通过OpenHarmony的Native API桥接到底层,让上层Dart代码尽可能复用。成语接龙这个项目,文字渲染、触摸事件、ListView滚动、Dialog弹窗,几乎不依赖特定硬件能力,是验证这套桥接最稳妥的试验场。
选择文字游戏还有一层考虑:Flutter在复杂动画和高频刷新场景下,对GPU和渲染管线的要求很夸张,而OpenHarmony的图形栈还在持续完善,如果一上来就做3D游戏,很容易把系统适配问题和自己代码问题混在一起。成语接龙界面大多是静态文本和简单转场动画,即使出现渲染异常,范围也容易锁定。对想接触OpenHarmony跨端开发的人来说,这类项目是最理想的起点。
跨端方案里也有React Native,但RN在OpenHarmony上的社区活跃度远不如Flutter。Flutter自己的渲染引擎不依赖原生控件,界面一致性比RN强;再加上dart:ui在同一版本下表现稳定,处理中文排版、字体回退也还算方便。实际开发中我发现,Flutter for OpenHarmony对第三方包的支持已经不错,dio、lottie、shared_preferences这些纯Dart或轻原生依赖的库都能跑通,但涉及平台通道的插件需要逐个验证,这也是我后面会展开的重点。
1.2 成语接龙的核心玩法拆解与功能边界
一个完整的成语接龙游戏,玩法可以很复杂,但作为项目落地,我先圈定了边界。基础版就两个角色:玩家和机器。游戏开始后,机器先抛出一个成语,比如“一心一意”,玩家必须输入一个以“意”字开头的成语,像“意气风发”。如果输入错误,界面给出提示,玩家可以重新输入;如果输入正确,机器再根据“发”字作答,循环直到一方接不上为止。计分规则暂时简单:接对一个加10分,使用提示扣5分,30秒内没接上算输。
围绕这个流程,我拆出了几个核心模块:成语词库、接龙判定、输入交互、倒计时逻辑、计分与历史记录、游戏结束页。词库模块负责加载和索引;判定模块负责校验成语是否存在、是否重复、首尾字是否匹配;交互模块包括TextField输入、软键盘控制、键盘遮挡处理;再往外才是网络扩展,比如在线成语查询和更新词库。
一开始我差点把“在线查询成语是否存在”当成首版必备功能,后来实际写代码才发现,如果每个输入都要等网络返回,用户体验极差,离线词库才是核心。于是我把网络模块放在第二优先级,首版以本地词库为主,网络只用来查询释义和扩展词库。这样设计的好处是,就算OpenHarmony设备暂时没有联网,游戏也能完整跑起来,也方便我在真机上测试核心逻辑,不会被网络问题干扰。
2. 开发环境搭建:编译器选型与常见坑
2.1 编译器:VS Code 还是 DevEco Studio
很多人会问“Flutter现在主流开发用什么编译器”,我的建议是看目标平台。如果只写纯Flutter应用,VS Code加Flutter插件完全够用,启动快、插件轻、Dart调试好用。但这次要跑OpenHarmony,处理器绕不开DevEco Studio工具链里的SDK、hvigor构建和签名配置。
我实际采用的是双工具组合:Dart逻辑和UI代码在VS Code里写,利用flutter analyze和flutter test做静态检查;OpenHarmony的工程外壳、签名、烧录交给DevEco Studio处理。这样做有一点点别扭,每次改动工程配置都要两边切换,但好处是Dart侧开发效率高,自动补全和热重载比DevEco Studio里强不少。如果你只打算下载官方OpenHarmony Flutter SDK,也可以用DevEco Studio直接导入Flutter工程,里面会自动识别Dart文件,只是热重载偶尔不及时,习惯之后也能接受。
环境安装上,我和大家一样先从OpenHarmony官方Gitee仓库拉Flutter分支,具体是flutter_flutter这个仓库的OpenHarmony定制版本。克隆后把它当成普通Flutter SDK用,设置好PATH环境变量,再额外准备OpenHarmony SDK,也就是DevEco Studio自带的那个。编译OpenHarmony的hap包时,需要把ohos SDK路径告诉Flutter工具链,否则它会找不到系统API。这一步文档写得不详细,我是参考社区的几个issue才跑通的。
2.2 环境报错实录与解决思路
开发过程中我遇到好几个论坛里高频出现的问题,第一个就是热词里那个“unable to find suitable visual studio toolc”。这个报错通常出现在Windows环境,Flutter编译OpenHarmony插件原生代码时,需要调用C++编译工具链。如果你的机器没装Visual Studio Build Tools,或者已经装了但版本不对,Clang找不到合适的工具链就会报这个。解决办法是先安装Visual Studio 2022 Build Tools,并勾选“使用C++的桌面开发”,然后打开"管理员命令行"运行flutter doctor,确认Clang被正确识别。
第二个经典报错是“You are applying Flutter's main Gradle plugin imperatively using the apply script”。这是Flutter旧版本在Android工程里的Gradle配置方式,OpenHarmony工具链沿用了一部分AGP逻辑,如果不兼容Flutter 3.22以上版本,就会提示这种规范问题。解决办法有两个:一是升级Flutter版本,二是手动改settings.gradle,把插件声明方式改为插件管理器的形式。我在一个老项目上就因为这个卡了整整一天,后来直接切到Flutter 3.24版本解决了。
另外还有个容易忽略的问题:Flutter SDK路径不能有中文和空格,否则编译时会莫名其妙地失败。我一开始把仓库放到D:\个人项目\flutter\下面,结果各种奇怪报错,后来改成D:\dev\flutter,一切正常。如果你在Windows上遇到玄学编译问题,先检查路径,再检查杀毒软件有没有拦截编译进程,这些都是很常见的坑。
2.3 OpenHarmony 画面渲染异常与 x86 模拟器问题
在OpenHarmony模拟器上跑Flutter应用,我遇到过一次典型的花屏问题,界面文字和色块错位。正常情况下,Flutter应用自绘渲染,不会出现这种问题。出现这种画面异常,大概率是图形栈适配问题,具体来说,是Flutter Engine和OpenHarmony的GPU之间的缓冲区格式不一致,导致画面撕裂或错位。我一开始以为是字库问题,后来排查发现,把DevEco Studio模拟器的硬件渲染关闭、改成软件渲染后,画面就正常了。但这并不是长久之计,真机上还是要开启硬件加速。社区里有人给出一个临时方案:在MainAbility里设置窗口背景为纯色,同时使用FlutterView的setBackgroundColor,能缓解部分花屏。
再说x86模拟器。OpenHarmony官方镜像很多是x86架构,但Flutter Engine的OpenHarmony版本优化重点是arm64,x86上的JIT和AOT表现都不太好,甚至有些版本直接不能启动。我的实际感受是,x86模拟器能跑但特别卡,首帧要好几秒。如果只是验证业务逻辑,还能接受;但想验证性能或者跑动画,基本不可靠。建议有条件直接上RK3568开发板或者最新的Dayu系列开发板,百元级成本,比模拟器靠谱得多。
3. 成语接龙核心逻辑与数据层实现
3.1 成语词库设计
成语接龙玩起来爽不爽,词库是关键。我收集了大约8000条常用成语,每条包含成语本身、拼音和常用释义。为了压缩包体和启动时间,先做了一轮筛选,过滤掉过于生僻和包含生僻字的条目,最终保留5000条左右,做成一个JSON文件放在assets目录里。
JSON结构我设计得比较扁平:
[ { "word": "一心一意", "pinyin": "yi xin yi yi", "meaning": "形容做事专心一意,没有杂念" }, { "word": "意气风发", "pinyin": "yi qi feng fa", "meaning": "形容精神振奋,气概豪迈" } ]光有word和pinyin还不够,接龙时如果每次都去字符串里抠第一个字、最后一个字,再查拼音,效率不高。所以我初始化了一个内存索引,结构是Map<String, List<Idiom>>,key是最后一个字的拼音,value是能接上的成语列表。比如“一心一意”的尾字是“意”,拼音是“yi”,那么索引里yi对应的列表就会包含“意气风发”。这样玩家输入“意气风发”后,我直接查它的尾字拼音“fa”,就能命中机器候选列表。
词库从assets加载后,我还会把它缓存到本地,用shared_preferences保存索引版本号。每次启动时先检查版本号,如果版本一致,直接从文件缓存读取;如果不一致,才重新解析assets里的JSON并重建索引。这样做的原因是assets每次打包进去的版本是固定的,更新词库不用重新发版,而是通过热更新拉新词库再覆盖缓存,后面网络模块里我会细说。
3.2 接龙判定规则与多音字处理
先看最简单的首尾字拼音判定。假设上一个成语是lastWord,玩家输入的是newWord,可以这样处理:
String getLastPinyin(String idiom) { // 从词库里取出对应成语的拼音,取最后一个空格后的部分 // 例如“一心一意” -> yi xin yi yi -> 最后一个拼音是 yi } String getFirstPinyin(String idiom) { // 取第一个拼音 } bool isChainable(String lastWord, String newWord) { return getLastPinyin(lastWord) == getFirstPinyin(newWord); }但这里有个大坑:多音字。比如“长”有chang和zhang两个读音,“行”有hang和xing。如果玩家输入“行云流水”接“水落石出”,而“行”在词库里的标准拼音是xing,但玩家实际上是读hang,那么按标准拼音判定会认为无法接龙,体验就很差。
我用了两种方式兜底。第一种是在词库里给多音字成语存多组拼音备选,比如“行云流水”可以存xing yun liu shui,同时额外加一个字段alternativeFirstPinyins: ["hang"]。第二种是动态判断,取用户输入成语的首字,与上一个尾字的所有读音取交集,只要交集不为空就判定通过。判定函数像这样:
bool canConnect(String lastIdiomPinyinList, String newWordFirstPinyinCandidates) { return lastIdiomPinyinList.any( (p) => newWordFirstPinyinCandidates.contains(p) ); }实际操作中,第二种方式更灵活,因为不用为每个成语维护备选拼音,只需要给常用多音字准备一个“首字读音候选表”。我把这个表单独放一个Map,加载词库时用它给每个词条补充首字候选读音,内存开销很小,但命中率高很多。
还有一个容易被忽略的规则:成语是否重复。连续接龙时,已经出现过的成语不能再用。我用一个Set<String>保存本局历史,判定时先查是否重复,重复则提示“这个成语已经用过了”。另外要判断用户输入的成语是否真的在词库里,不在词库但拼音规则符合也不能放行,否则玩家可以随便造词。
常见错误处理都放在一个函数里,方便界面层直接调用:
enum CheckResult { ok, notIdiom, repeated, notChainable } CheckResult checkMove(String lastWord, String newWord) { if (!_idiomIndex.containsKey(newWord)) return CheckResult.notIdiom; if (_usedSet.contains(newWord)) return CheckResult.repeated; if (!canConnect(_tailPinyinMap[lastWord], _headPinyinCandidates[newWord])) { return CheckResult.notChainable; } _usedSet.add(newWord); return CheckResult.ok; }3.3 用 Flutter isolate 优化词库加载
5000条成语的JSON文件也就几百KB,听起来不大,但如果启动时直接在主Isolate里解析并建立索引,在OpenHarmony开发板上耗时可能会到500ms到1秒。在游戏启动时用户会盯着白屏,这个体验非常差,所以我把词库加载放到了后台Isolate里。
最简单的方式是用compute函数,把文件读取和解析都传给后台Isolate执行:
Future<DictionaryData> loadDictionary() async { final rawString = await rootBundle.loadString('assets/idioms.json'); return compute(parseIdioms, rawString); } DictionaryData parseIdioms(String raw) { final list = jsonDecode(raw) as List<dynamic>; final index = <String, List<Idiom>>{}; final tailPinyinMap = <String, List<String>>{}; final headCandidates = <String, List<String>>{}; // ...解析和建立索引 return DictionaryData(index, tailPinyinMap, headCandidates); }为什么建议用String传参,而不是直接传List<dynamic>?因为compute在传递对象时会做拷贝,如果传一个已经解析出来的大List,底层序列化成本很高,反而拖慢速度。先读成String,后台解析,能减少一次大对象拷贝。
词库加载期间,我通常会显示一个简单Loading页,等loadDictionary返回后再进游戏页。这比在游戏页里异步加载要更清晰,也方便做进度提示。后来我为了更细致地做内存优化,把DictionaryData设计成不可变对象,加载完成后不会再改动它的引用,让GC可以有效回收加载过程中产生的临时Map。这一点在下一节还会继续讲。
4. 网络模块、动画与体验增强
4.1 Dio 请求封装与在线成语校验
本地词库再大也覆盖不了所有成语,所以我想加一个“在线校验”功能:当用户输入一个本地词库不存在的四字词时,通过在线接口查询它是否属于标准成语,如果属于,就允许接龙,同时把这个词条缓存到本地,作为整个游戏的动态词库补充。
网络库我选dio,因为Flutter社区里它的拦截器设计和文件上传下载支持都比http要好。封装的核心是统一BaseOptions和拦截器:
class ApiClient { static final ApiClient _instance = ApiClient._(); late final Dio _dio; ApiClient._() { _dio = Dio(BaseOptions( baseUrl: 'https://api.example.com/v1', connectTimeout: const Duration(milliseconds: 5000), receiveTimeout: const Duration(milliseconds: 5000), )); _dio.interceptors.add(LogInterceptor(responseBody: true)); _dio.interceptors.add(InterceptorsWrapper( onRequest: (options, handler) { // 加token、公共参数 handler.next(options); }, onError: (DioException e, handler) { // 统一错误码映射 handler.next(e); }, )); } }这里有一个经验:不要把每个请求都直接在页面里new一个Dio,否则日志、token、超时都要重复配置。用单例封装之后,后续加接口只需要新增方法,比如查成语释义:
Future<IdiomDetail> fetchIdiomDetail(String word) async { final resp = await _dio.get('/idiom/detail', queryParameters: {'word': word}); return IdiomDetail.fromJson(resp.data); }在处理接口返回时,我比较习惯用Result类型包一层,统一处理成功、失败、网络异常,而不是让异常散落在业务代码里。尤其成语接龙这种交互频繁的场景,一个网络超时如果不能被合理捕获,用户会直接卡在输入框里。我后来总结出的原则是:网络请求只负责返回数据或标准错误,业务层关注错误码,界面层关注提示文案。
4.2 用抓包调试定位网络问题
潮汕语录里有一句“得真功夫”,做Flutter联调,抓包也是真功夫。第一次接入在线成语查询时,我发现模拟器上请求总是超时,但接口在浏览器里是好的。后来用Charles抓包发现,请求根本没发到服务器,而是被Flutter默认的Socket连接方式卡住了。原因是我修改了系统HTTP代理,但Dio默认没有读取Flutter侧的代理设置,导致流量没有走Charles监听端口。
解决方案是在Dio初始化时自定义HttpClient的代理:
(_dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () { final client = HttpClient(); client.findProxy = (uri) { return 'PROXY 127.0.0.1:8888'; }; return client; };这是开发模式的写法,打包发布前一定要注掉,否则会把所有流量强制指向本地代理,用户没开代理就直接报错。抓包时还要注意证书信任:Charles根证书要先安装进手机或开发板,否则HTTPS请求会被认为不安全而失败。如果你用的是真机,可以通过adb推送证书,OpenHarmony上类似,但权限管理更严格,有时候需要root。
另外,我还会在开发环境里加一个?debug=1参数,让后端返回mock数据,这样没有网络也能测试各种分支。这个习惯帮我排查出很多界面逻辑问题,而不是每次都依赖在线接口。
4.3 Lottie 加载网络动画资源
游戏里我加了两个Lottie动画:开始页的“龙”主题动画和接龙成功时的胜利动画。本来Lottie可以直接加载assets里的json文件,但我想让动画能动态更换,不用每次发版,所以做成从网络下载zip包再解压加载的方式。
LottieFlutter这个库的Lottie.asset只支持本地文件或assets路径,不能直接用Lottie.network加载一个zip包,因为zip包不是单json文件。所以我用archive包先把zip解压,拿到里面的json文件,写到应用缓存目录,再通过Lottie.asset加载。
关键代码像这样:
Future<File> loadLottieZip(String url) async { final zipFile = await downloadZip(url); final destinationDir = await getApplicationCacheDirectory(); final extractDir = Directory('${destinationDir.path}/lottie_extract'); if (!extractDir.existsSync()) extractDir.createSync(recursive: true); final archive = ZipDecoder().decodeBytes(zipFile.readAsBytesSync()); for (final file in archive.files) { if (!file.isFile) continue; final outp = File('${extractDir.path}/${file.name}'); outp.createSync(recursive: true); outp.writeAsBytesSync(file.content as List<int>); } return File('${extractDir.path}/animation.json'); }这个方案有两个坑。第一个是zip包内目录结构可能在服务器上变了,代码里写死animation.json就会找不到文件,我在服务器端固定了包内文件名,同时增加了动态扫描逻辑:找不到时先列出目录下所有json文件,取第一个。第二个坑是内存:解压下载的zip如果超过10MB,一次性读入内存很容易在低端开发板上OOM,所以我用流式写入,而不是readAsBytesSync全部读进内存。如果你只是做demo,可以用简单方式,但想上线,流式处理是必须的。
5. 内存优化与多 Flutter 版本管理
5.1 内存优化思路
在OpenHarmony开发板上跑Flutter,内存优化真不能等到界面卡顿才想起。成语接龙虽然看起来轻量,但词库索引、Lottie动画、日志缓存都可能造成内存压力。我第一次在RK3568板子上测试时,游戏连续玩二十局后,系统内存占用从200MB涨到500MB,最终被系统杀掉。
排查时先用DevTools的Memory视图抓了几组内存快照,发现主要问题有两个:一是TextEditingController没有在页面销毁时释放,每个对局页面都在创建新的Controller,退出后旧页面被路由栈保底保留,但实际上已经不再需要,这会导致内存持续堆积;二是一次性把整个词库索引全部放在内存里,虽然数据量不大,但加上运行时副本,在OpenHarmony这种内存管理偏保守的系统上还是有压力。
优化措施有三个。第一,用AutomaticKeepAliveClientMixin不再无脑缓存页面状态,而是根据游戏结束状态判断是否销毁。第二,下载的Lottie动画在动画播放结束后立刻释放资源,调用Lottie.asset时显示的配置frameRate和repeat都不保留长引用。第三,词库索引改为懒加载,第一次点击输入框时才建立尾字索引,之后保留但定期清理不再使用的临时Map。
另外提一下Dart内存分配策略。Flutter的GC在大部分情况下不需要人工干预,但如果你发现自己不断创建大量短命对象,GC频繁触发,帧率会掉。我在构建接龙候选列表时,把原来每局都新建的List<Idiom>改成了复用同一个List,先清空再填充。这个改动看起来不起眼,但在低端设备上让帧时间从300ms降到100ms以内,效果非常明显。
5.2 用 FVM 安装与管理多版本 Flutter
做OpenHarmony适配时,我还遇到了一个头疼问题:团队其他项目用的是Flutter 3.10,而OpenHarmony分支需要Flutter 3.24,因为新版引擎才修复了一堆图形栈bug。多个版本来回切换,如果每次手动改PATH,很容易切完就忘,还容易把Dart SDK缓存搞乱。后来我用了FVM。
FVM(Flutter Version Management)是一个命令行工具,可以针对不同项目锁定Flutter SDK版本。安装好FVM后,一般几个命令就够了:
fvm install 3.24.0 fvm install 3.10.0 fvm use 3.24.0在项目根目录会生成一个.fvmrc文件,里面记录了当前项目使用的Flutter版本。下次团队成员拉代码后,直接运行fvm use就能把本地SDK切到对应版本。如果你是VS Code用户,也可以把SDK路径指向~/fvm/versions/3.24.0,然后重启窗口,这样Dart插件也能识别对应版本。
这里要注意,FVM只是管理Flutter SDK,它不会自动处理OpenHarmony的SDK路径。所以我在项目里写了一个初始化脚本,用环境变量OHOS_SDK_HOME指向DevEco Studio的SDK目录,然后在fvm flutter build hap之前先执行这个脚本。这样比手动配置稳定。
5.3 进阶提问:从实战到面试
做完这个项目后,我发现很多经典的Flutter面试题都能拿它当案例回答。比如面试官问“Flutter中为什么用isolate而不是线程”,我会说在成语接龙里,词库加载就是一个典型的耗时计算任务,放在UI Isolate会导致掉帧,而Isolate之间通过消息传递,可以避免共享内存竞争,更适合这种数据独立的解析任务。
再比如“如何优化Flutter启动白屏”,我会提到三个层面:冷启动阶段的原生层白屏需要用FlutterView的background color覆盖,Dart层则通过优化首帧构建路径,把词库加载从启动流程中挪走;还有延迟初始化,游戏页点击开始前才去加载Lottie资源和在线配置。这样首帧可以压缩到1秒以内。
“Hot Reload不生效怎么办”这种问题也一样,我在OpenHarmony上遇到过Hot Reload状态丢失的情况,主要原因是在DevEco Studio里改的是原生工程,Dart代码虽然改了,但OpenHarmony的构建系统没有触发增量编译。解决方案是用命令行工具单独跑fvm flutter run -d <device>,而不是完全依赖IDE按钮。这些细节说多了,面试官会知道你真的踩过坑。
6. 常见问题与排查技巧实录
6.1 高频报错速查表
我整理了这次开发遇到过的高频问题,做成表格,方便大家直接对照。
| 问题现象 | 根本原因 | 解决办法 |
|---|---|---|
编译报unable to find suitable visual studio toolc | Windows环境缺少C++编译链 | 安装VS Build Tools C++开发组件,重启环境 |
Gradle构建报You are applying Flutter's main Gradle plugin imperatively | Flutter Gradle插件被旧方式应用 | 升级Flutter或迁移到plugin management |
| OpenHarmony模拟器画面花屏/错位 | Flutter Engine与软件渲染不兼容 | 关闭GPU硬件加速,或使用真机验证 |
| x86模拟器运行卡顿、偶发崩溃 | Flutter Engine对x86优化弱 | 使用arm64真机或调整模拟器镜像 |
| Dio抓包看不到请求 | 未设置findProxy | 初始化时指定代理地址,并安装Charles证书 |
| Lottie加载网络zip包失败 | 缺少解压或路径不匹配 | 用archive解压,并按目录扫描json文件 |
| 内存持续上涨,最终卡死 | 页面未释放Controller或资源未清理 | dispose中释放,避免页面级缓存 |
如果还有更冷门的报错,我一般会结合Flutter日志和OpenHarmony日志两个方向去查:flutter logs只打印Dart层和部分插件日志,hilog则能看到Native层崩溃栈。两个日志对照,基本能定位80%的问题。尤其在OpenHarmony上,Flutter引擎崩溃时Dart层日志可能毫无异常,必须去看hilog。
6.2 我的调试心得与避坑建议
首次在OpenHarmony上跑Flutter,建议大家一定改掉“先在模拟器上把全部功能做完再上真机”的想法。模拟器能暴露的逻辑问题有限,像渲染异常、内存压力、多音字判定这些都是真机上才明显。成语接龙项目我大概有一半时间花在调试环境适配和性能排查上,而不是游戏规则本身。
词库方面,建议前期先用几百条成语做冒烟测试,把接龙判定规则跑通后再导入完整词库。因为多音字和生僻字问题只会在大量数据下爆发,小词库会让你误以为规则写对了。等词库扩到5000条后再重新审视判定,效果会好很多。
说到调试日志,不要全部用debugPrint,它会在release模式默认被优化掉,导致线上问题无从查起。我给项目加了一个简单的Logger单例,统一控制日志开关,Dio拦截器也复用了它。这样在真机调试时,把开关打开;发布时,一行配置把所有网络日志关掉,既能保护用户隐私,也能减少I/O压力。
最后一个建议是:先做离线,再做在线;先跑通玩法,再优化体验。把词库、判定、UI交互这几个离线核心模块打磨干净,再慢慢接入在线校验、Lottie动画、动态词库,整体风险会小很多。否则一上来就是网络请求加动画,遇到渲染异常你根本不知道是自己代码的问题还是引擎适配的问题。这次的成语接龙项目,我最终在OpenHarmony和Android双端都顺利跑通,虽然过程中踩了不少坑,但每次调试完都把日志和报错信息整理成表格,后面团队再做类似项目时,直接拿这份速查表就能少走弯路。
我在实际开发里最大的体会是:跨端不是“跑起来就行”,而是要真正把你习惯的Flutter开发流搬到新平台上。OpenHarmony的生态还在快速变化,Flutter for OpenHarmony也不例外,今天能跑的插件明天可能因为SDK升级又出问题。遇到问题别慌,先看官方Gitee仓库的issue区,再结合日志定位,大部分坑都是能绕过去的。如果你也想做类似尝试,建议从文字游戏这种轻量项目下手,既能快速出成果,又能把环境、渲染、内存这些关键路径全部摸一遍,之后再去做复杂应用就有底气多了。