很长时间没写鸿蒙相关的东西了,今天聊一个偏工程的话题:把一个 Flutter 脚手架工具做成鸿蒙化。准确说,是flutter_architect_cli这套架构模板资产管理工具,如何从传统 Flutter 工程平滑迁移到鸿蒙工程体系。
这个工具我前后用了一年多,从最开始只是维护几套模板文件,到后来变成团队所有 Flutter 项目的统一入口,包括模块创建、架构分层、路由注册、代码生成甚至后续的规范检查,都靠它一条命令完成。这次适配鸿蒙,并不是推翻重来,而是把原本只认识 Flutter 工程结构的 CLI,扩展成同时认识 stage 模型、module.json5、方舟编译单元的“双语”工具。整个过程踩了不少坑,也总结了一些非常具体的操作经验和决策逻辑,整理出来分享给遇到同样问题的朋友。
1. 先认清一个事实:鸿蒙化不等于“重新写一套”
1.1 flutter_architect_cli 到底在治理什么
想搞清楚鸿蒙化适配该怎么做,先要明白这个工具在普通 Flutter 工程里扮演的角色。简单说,flutter_architect_cli是一个专门负责“生成和治理架构资产”的命令行工具,它做的事情包括:
- 根据参数生成 feature 模块目录,包括 data、domain、presentation 三层基础结构;
- 写入标准化的路由表、依赖注入配置、环境变量配置;
- 产出基础模板代码,例如 Bloc/Cubit 状态管理样板、Repository 实现骨架;
- 扫描已有工程结构,检查目录规范、命名规范、是否缺失必要文件;
- 支持模板中变量替换,让产出代码带上模块名、包名、作者信息等自定义内容。
最核心的设计思想是“约定优于配置”。团队里所有 Flutter 项目的目录结构高度统一,风险点大幅减少,新成员看代码的负担也小了很多。模板资产全部集中在 CLI 维护,项目本身不存模板,只存生成结果,这样任何结构调整都能一次性生效。
但到了鸿蒙这里,问题就来了。鸿蒙工程不是一个纯粹 Flutter 工程,它同时存在 HAP(HarmonyOS Ability Package)模块和 Flutter 侧的 lib 模块。至少从工程结构层面看,一个完整的鸿蒙 Flutter 应用会包含:
AppScope目录,存放应用级配置和资源;entry模块,也就是鸿蒙侧的入口模块,包含 ets 页面、ability 配置、module.json5;ohos目录或适配层,承载 Flutter 引擎加载和原生能力桥接;- Flutter 侧的
lib目录,包含 Dart 代码和平台通道定义。
如果 CLI 还是按旧逻辑生成纯 Flutter 结构,产出的内容放到鸿蒙工程里是完全跑不起来的。适配工作的核心,就是让模板和生成逻辑同时认识这两套工程体系。
1.2 鸿蒙化和跨平台复用并不矛盾
很多人的第一反应是:鸿蒙不是有自己的 ArkTS 和 ArkUI 吗,为什么还要专门适配 Flutter CLI?
这个想法有一个盲区。鸿蒙生态里 Flutter 的定位不是替代 ArkUI,而是给已经积累了 Flutter 代码库的团队提供一条低成本迁移路径。业务层逻辑、状态管理、数据模型、网络层,这些跨平台能力完全可以复用;真正需要适配的只是工程外壳和平台桥接层。flutter_architect_cli的使命就是把“工程外壳”这部分自动生成为鸿蒙规范,同时保持业务层模板不变。
我在适配过程中反复和团队强调一个原则:Dart 层代码尽量零改动,平台层和工程层代码全部走模板变量。比如同一个 Repository 模板文件,在标准 Flutter 工程里生成到lib/modules/xxx/data/repository/,在鸿蒙工程里依然生成同样的相对路径,CLI 自动感知工程类型后决定是否额外生成ohos侧的桥接文件。这样既减少了维护分支,又保证了跨端一致性。
2. 动手之前,先做资产盘点
2.1 模板资产到底有哪几类
这是适配前最重要的一步。如果不知道 CLI 手里管了多少种资产,就贸然改逻辑,后面一定会出现“生成到一半发现缺文件”的尴尬局面。我把手头的资产分成了三类,划分标准是“鸿蒙化影响程度”。
第一类:纯 Dart 资产。包括状态管理模板、仓储模式模板、网络请求封装、路由配置、utils 工具类等。这些文件不依赖平台能力,鸿蒙和标准 Flutter 之间基本一致。适配成本最低,只需要检查导入路径和包名替换规则。
第二类:工程结构资产。包括 pubspec.yaml 模板、分析选项配置、目录骨架、Git 忽略文件、CI 配置模板等。这类资产直接和工程形态绑定,是鸿蒙化改动最大的部分。例如 pubspec.yaml 中需要增加鸿蒙平台相关的配置段,目录骨架需要支持AppScope、entry、ohos等额外层级。
第三类:平台桥接资产。包括 MethodChannel 定义、事件通道封装、原生侧实现骨架、权限声明模板。这类资产在标准 Flutter 工程里往往只露一个 Dart 侧接口,原生代码要靠开发者手动补,但在鸿蒙化场景下 CLI 完全可以生成 ArkTS 侧的通道注册代码。
分类完成后,我列了一张映射表,记录每个模板文件在当前工具里的模板 ID、输出路径、受影响类型、适配优先级。这个表后面起到了非常大的作用,因为改动过程中经常要回过头核对“有没有漏掉的受影响文件”。
2.2 明确边界:什么不该改
盘点资产的同时也要划定边界。我见过一些人做鸿蒙化适配,恨不得把所有模板都加一层鸿蒙判断,最后模板数量翻了一倍,维护成本剧增。正确的做法是区分“框架公共资产”和“平台相关资产”。
比如路由表这类纯映射资产,不应该因为鸿蒙而改变。数据层模板也不应该出现ohos条件分支。真正需要条件分支的,只有入口文件、module 配置、平台通道注册、资源引用方式这几类。
另一个边界是“生成逻辑”和“校验逻辑”的分离。老版本 CLI 在工程生成后会自动执行flutter analyze,但鸿蒙工程的静态检查不仅涉及 Dart,还涉及 ArkTS 侧以及模块配置。强行复用旧逻辑可能导致检查过程报出大量与业务无关的鸿蒙 IDE 提示。我的处理方式是把“格式校验”拆成两步:先生成,再做一次结构自检,Dart 侧静态检查和鸿蒙模块配置校验分开跑,避免相互干扰。
3. 模板资产改造:从 Flutter 单一结构到双模结构
3.1 目录骨架的迁移要点
旧版模板的目录结构大概是这样的:
project_name/ ├── lib/ │ ├── core/ │ ├── features/ │ ├── routes/ │ └── main.dart ├── pubspec.yaml ├── analysis_options.yaml └── test/适配鸿蒙后,目标结构变成了:
project_name/ ├── AppScope/ │ ├── app.json5 │ └── resources/ ├── entry/ │ ├── src/main/ │ │ ├── ets/ │ │ ├── resources/ │ │ └── module.json5 │ └── build-profile.json5 ├── ohos/ │ └── FlutterBridge/ ├── lib/ │ ├── core/ │ ├── features/ │ ├── routes/ │ └── main.dart ├── pubspec.yaml ├── oh-package.json5 └── build-profile.json5这个结构看起来复杂,其实每层的职责非常明确。最关键的是entry模块和ohos适配层。CLI 生成时不需要替开发者写业务 Ability,但至少要产出:
- 一个最小可运行的
EntryAbility.ets模板; - 加载 Flutter 容器所需的页面代码;
- 注册原生插件通道的初始化代码;
- module.json5 里必要的能力声明。
我采用的方案是在模板目录里增加一套__harmony__前缀的模板子目录。生成器检测到--platform harmony参数后,自动把前缀目录合并进输出,同时保留原有目录结构不变。这样设计的好处是静态资产可以直接复用,不需要为鸿蒙重写整套模板引擎。
3.2 平台通道代码的管理方式
平台通道是 Flutter 跨端能力的重要桥梁,鸿蒙化适配时这块的改动最容易被忽略。很多 Flutter 项目在标准端用了成熟的 channel 插件,代码里写的是:
const MethodChannel('com.example.device_info');标准 Flutter 端一切正常,但鸿蒙工程的通道注册入口完全不同。ArkTS 侧需要手动创建一个FlutterPlugin或PlatformChannel注册类,在里面写对应的方法映射。CLI 没法替你完成所有业务 channel 的迁移,但它可以做到三件事:
第一,在生成 Dart 模板时,将 channel 名称抽取为统一常量,避免字符串散落到各个业务文件里,后续鸿蒙侧对齐时只需要引用同一份常量文件。
第二,生成 ArkTS 侧的通道骨架文件,包括onMethodCall的分发结构和未处理方法的默认返回。开发者后续只需要填充真正需要实现的 case 分支,不需要从零搭建通道框架。
第三,自动在 module.json5 里注册权限或依赖声明。很多开发者在鸿蒙侧遇到“明明代码没问题但插件调用失败”,几乎都是配置声明缺失。CLI 层面直接补齐,能避免八成类似问题。
我自己实践时给模板加了一个“通道清单”概念:在项目根目录维护一个bridge_config.yaml,CLI 读取后同时生成 Dart 侧常量表和 ArkTS 侧注册代码,两边始终同步。
3.3 模板变量与条件渲染的治理
模板引擎是整个 CLI 的灵魂,它决定了生成能力的天花板。早期版本我用的变量替换很原始,基本就是正则替换。适配鸿蒙后,条件渲染变得非常重要,因为同样一个功能在标准 Flutter 和鸿蒙工程里的输出文件完全不同。
我在引擎里加入了三类新语法:
{{#harmony}}块:仅在鸿蒙模式下输出块内内容;{{#flutter}}块:仅在标准 Flutter 模式下输出;{{moduleNameCamelCase}}等扩展变量:处理命名风格转换。
以入口文件为例。标准 Flutter 项目的main.dart是一个直接调runApp的简单结构,但鸿蒙场景往往需要一个引导入口,先初始化 Flutter 引擎绑定,再加载容器页面。通过条件渲染,同一个模板文件可以同时产出两套入口逻辑,不维护双份文件。
条件渲染这块有个坑很容易被忽视:模板里嵌套的缩进。因为{{#harmony}}包裹的内容会被整体输出,如果模板里不注意缩进,生成出来的代码会出现混排,Dart 侧格式检查直接崩溃。我建议生成后统一接一遍格式化工具,不管 dart format 还是 ArkTS 的格式化,都作为生成管线里的固定步骤。
4. 脚手架治理实战:让 CLI 自己先“鸿蒙化”
4.1 命令设计上的鸿蒙参数
工具改造完了,接下来是命令入口的治理。旧版本的命令可能长这样:
architect create feature --name user --state bloc适配后,我保留了原命令的兼容性,同时新增平台参数:
architect create feature --name user --state bloc --platform harmony--platform选项的取值目前支持flutter和harmony。默认值是flutter,所以老项目升级工具后不会打破原有行为。命令内部会先检测当前工程类型,再决定使用哪套模板布局。
这里有一个值得交代的设计细节:--platform不是单纯的开关,它还会影响后续所有子命令的行为。比如architect channel add在 harmony 模式下会同时生成 Dart 常量和 ArkTS 注册文件,而architect route add在 harmony 模式下只更新 Dart 路由表,不触达鸿蒙侧的 config。
命令参数的命名也经过了几轮调整。一开始用--ohos,发现团队里很多人不知道 ohos 指什么,后来改成--harmony,语义立刻清晰了。命名这种事看着小,实际影响工具的可接受度。
4.2 生成后的自动化检查与格式化
模板的增量改造完成后,工具治理开始进入“自动化”阶段。核心思路是让 CLI 不只是一次性脚手架,而是能持续治理工程结构变化的管家。
我在 CLI 里增加了一个doctor子命令,用法类似:
architect doctor --platform harmony它会做四件事:
- 检查当前目录是否具备鸿蒙 Flutter 工程的基本结构;
- 核对 AppScope、entry、ohos 目录是否存在且非空;
- 检查
oh-package.json5和build-profile.json5的依赖声明是否一致; - 扫描 lib 目录中是否有违反团队约定的类名或文件名。
这个命令解决了一个很实际的痛点:团队里经常有人手快,生成完模板后手动删了几个文件,导致后续编译期报错。有了自动化自检,问题在编码阶段就能暴露。
格式化和 lint 的接入同样重要。鸿蒙侧代码的格式化工具跟 Flutter 不同,不能一个命令通吃。CLI 的解决思路是设计一个“格式化适配层”,在 harmony 模式下自动调用 ArkTS 的格式化工具,在 flutter 模式下继续使用 dart format。这一步属于典型的“小事不小”,能减少大量因格式差异导致的 git diff 噪音。
4.3 和工程治理流程的整合
最后是流程整合。CLI 鸿蒙化的价值如果只停留在模板生成层面,那还远远不够。我真正看重的是它能否融入团队现有的工程治理链路。
我将工具和 CI 流程做了结合。每次开发者提交代码前,本地钩子会先执行一次architect doctor,如果出现结构性问题,直接阻断提交,并且给出修复提示。这个过程并不检查业务逻辑,只做工程结构和配置文件层面的校验,所以速度很快,不会成为开发负担。
还有一个很实用的功能是“增量生成”。老版本 CLI 每次创建模块都会把整套模板重新生成一遍,这在纯 Flutter 工程里问题不大,但在鸿蒙工程里会非常危险,因为一旦覆盖了开发者已经改过的 ArkTS 代码,轻则丢失冲突,重则整个模块编译不过。所以我在治理层面做了一个重要调整:只有新增文件时全量生成,已有文件采用合并策略,对于 channel 映射和路由表采用追加写入的方式。
这个调整经历过一次事故。最初为了省事我用了整体覆盖,结果某位同事在生成的桥接文件里加了十几行自定义逻辑,第二次生成时全部被清掉了,花了很长时间排查才发现是工具覆盖导致。所以这里特别提醒:治理型 CLI 一定要把“覆盖”作为高危操作,默认宁可多生成几个补丁文件,也不要动开发者已有的产出。
5. 常见问题与排障实录
5.1 IDE 里刷不出鸿蒙模块
这是适配初期碰到最多的问题。CLI 生成完了目录,文件都在磁盘上,但打开 DevEco Studio 后工程树里看不到entry或ohos模块,或者看到的是灰色不可编译状态。
排查后发现问题集中在build-profile.json5的模块注册上。鸿蒙工程能正确识别模块,靠的是工程根build-profile.json5里modules数组的声明。CLI 生成时如果只创建目录、不更新这个数组,IDE 自然无法感知新增模块。
解决办法是在模板生成流程的最后一步,由 CLI 自动读取并合并modules配置。模板里提供一段 JSON5 片段,生成器扫描已有声明,若不存在同名字段则进行追加。这里还要注意 JSON5 和 JSON 的差异,严格 JSON 解析方式会在注释和尾逗号上直接报错,必须用兼容 JSON5 的解析方式处理。
5.2 模板里写死路径导致编译失败
这个问题带有一定的隐蔽性。CLI 老版本中为了图省事,在模板里写了一些相对路径,例如../../core/network/,在标准 Flutter 目录层级里没问题,但鸿蒙目录层级更深,实际编译时路径直接跳出模块根目录,编译报错非常难懂。
定位这类问题有个技巧:生成完成后立刻执行一次模块内 grep,把所有超出模块边界的相对路径引用找出来。我在工具里加了一个简单的“路径合法性检查”,它先解析模板输出的文件树,然后逐文件检查每个相对路径的真实落点,一旦发现路径逃逸,立刻在命令行输出警告。
经验总结下来,模板设计阶段就要确立一条铁律:所有 import 和资源引用都要基于包名或模块根路径来写,禁止使用多级相对路径。
5.3 平台通道注册失效
这个问题的现象是:Dart 侧调用某个 MethodChannel,没有任何报错,但就是没有返回值,超时也不触发。说是失效,其实代码都执行了,只是 ArkTS 侧没有注册对应的方法。
关键在于鸿蒙侧的插件机制和标准平台差异很大。标准端可能只要求实现一个类,鸿蒙侧除了实现类,还需要在初始化阶段主动把这个类实例传给 Flutter 引擎容器。CLI 生成的模板如果只生成了类文件,但漏了注册代码,问题就会在运行时暴露。
我在模板里设计了“强制注册段”:所有由 CLI 生成的 bridge 文件都从统一入口导入,入口文件里集中调用registrar方法。这样新增一个 channel 不需要开发者手动改入口,重新执行 CLI 生成即可。
5.4 资源文件在鸿蒙里找不到
Flutter 项目里图片、字体等资源可以放在 assets 目录,标准 Flutter 下声明在 pubspec 里就行。鸿蒙场景下,如果资源要同时被 ArkTS 侧使用,需要放到entry/src/main/resources下,并且资源引用方式不同。
这个问题的坑在于:CLI 生成的 Dart 模板可以正常引用 assets 里的图片,但 ArkTS 侧模板引用的资源路径却找不到文件。排查后确认是 resource 目录默认只识别base子目录,模板把资源放错层级,导致编译期报 resource not found。
修复方案是生成阶段统一维护一份“资源映射表”,CLI 根据目标平台决定资源应该落到哪个资源目录。如果是纯 Flutter 侧使用的资源,走原逻辑;如果是双端共用,则生成两份,并分别在两侧做出声明。
5.5 版本升级带来的兼容性问题
鸿蒙生态迭代速度比较快,工具适配时还要考虑版本漂移问题。旧版本的 Flutter 鸿蒙支持包和最新版本在某些 API 上不兼容,CLI 生成的代码可能用的是旧 API 签名,导致生成后无法直接编译。
我采取的策略是在 CLI 里维护一份“目标版本清单”,每次生成模板时附带当前工具版本对应的 API 基线。后续如果升级到新版本,可以执行一条迁移命令,工具自动扫描出项目中受影响的调用位置,并批量替换成新写法的模板片段。
这个功能还比较粗糙,不能覆盖所有业务代码,但至少解决了工程侧升级时的基础适配问题。
6. 模板资产管理的几个细节心得
这部分更像实践笔记,是我在整套鸿蒙化适配过程中沉淀下来的一些具体操作经验。
第一,模板文件命名统一加前缀。我在模板仓库里用_flutter_和_harmony_区分不同平台的文件,避免生成时混淆。公共模板则放在_common_目录下,生成器优先处理公共模板,再根据平台参数决定是否拉取对应平台文件。
第二,变量命名尽可能生成多风格版本。同一个模块名,在 Dart、ArkTS、JSON5、路径目录里需要的命名风格不同。我在变量系统里实现了snake_case、camelCase、PascalCase、SCREAMING_SNAKE_CASE四种转换,模板里按需取用。这个看起来基础,但实际省掉了大量手写替换逻辑。
第三,给 CLI 加一个「模板预览」参数。开发模板的人常常不知道改了模板后实际生成效果怎样。我增加了一个--dry-run选项,只输出文件树和关键变量的最终值,不改动磁盘。调试模板时非常好用,也能避免反复生成又删除对工程造成的干扰。
第四,善用模板片段继承。鸿蒙侧有很多配置文件结构相似,比如不同模块的 module.json5 只在一部分字段上有差异。模板系统如果支持片段继承,可以把公共配置部分抽成 base 片段,不同平台或不同模块类型通过覆盖少量字段来生成最终文件。后续配置规范调整时,只改 base 片段即可。
7. 个人实操中的一点体会
整套适配做下来,我的一个强烈感受是:鸿蒙化适配与其说是技术问题,不如说是“抽象边界”的问题。模板资产和工程结构之间的关系,必须想得非常清楚,哪些属于业务公共资产、哪些属于平台差异资产,这条线画得越清晰,后面维护越省心。
我原先的做法是“遇到一个平台差异就加一个 if 分支”,结果模板代码里散落着大量平台判断,读起来非常痛苦。后来推倒重来,把所有差异点收敛到三处:平台参数解析、模板目录选择、产物后处理。模板内部尽量减少平台判断,让模板本身保持干净。这个重构带来的收益非常明显,团队里其他成员阅读和修改模板的难度大幅下降。
再有就是要允许工具的“非完美适配”。CLI 的目标不是把鸿蒙所有特性都封装成模板命令,而是保证一个基础工程能快速落盘、能编译、能跑通道。过于复杂的鸿蒙特性,交给开发者在生成结果上再扩展,比强行塞进 CLI 要好得多。
还有一个实际操作上的小技巧:所有模板仓库都做版本标签,每个标签对应一组经过验证的 Flutter 版本和鸿蒙支持版本。团队里如果有人升级环境后生成出来的工程编译不过,第一件事不是翻模板代码,而是比对标签版本,能省下大量排查时间。
如果你现在正准备把自己团队的 Flutter CLI 工具做成鸿蒙化,我的建议是:先不要急着写模板,先把现有的每个模板文件放在“标准 Flutter、鸿蒙 Flutter、纯 ArkTS”三个场景下分别过一遍,弄清楚哪些是真正共享的,哪些只属于某一个场景。这一步做完,后面的改造路径会清晰很多。