从标题里的“2026”说起。我最近连续接触了几个从 Flutter 跨端转 OpenHarmony 的项目,发现大家拿到 OpenHarmony 工程后,第一反应都是先找 ohos 目录在哪,然后再去看 DevEco Studio 那一套配置。这个思路没毛病,但很多人卡住并不是因为不会写 Dart 代码,而是对 OpenHarmony 侧工程结构不熟,不知道哪些文件是系统生成的、哪些需要手动改、哪些改错了会导致编译直接崩。这篇文章就把 Flutter 编译开发 OpenHarmony 的全套工程目录结构拆开揉碎,从根目录一路看到编译产物,顺带把 RK3568 设备树选择、Gradle 插件报错、依赖下载失败这类高频坑一次说清楚。
这篇文章适合谁看?一个是被公司要求把现有 Flutter App 移植到 OpenHarmony 上、但第一次接触 OpenHarmony 工程的客户端开发;另一个是看了一堆 Flutter 教程但始终没搞清楚 entry、hvigor、module.json5 这些文件到底干什么的初学者。不管你是哪种,读完这篇以后,拿到一个 Flutter + OpenHarmony 工程可以先花十分钟把目录结构过一遍,基本就能判断这个工程的改造状态和编译风险。这不是什么高深的底层原理,就是一份工程实操地图。
1. 项目背景与整体设计思路
1.1 为什么 Flutter 要跑在 OpenHarmony 上
先说背景。OpenHarmony 是一个面向多设备、全场景的分布式操作系统,现在国内外不少设备厂商都在基于它做自己的发行版,比如直接跑在 RK3566、RK3568、RK3588 这类开发板上。而 Flutter 最大的价值在于一套 Dart 代码同时交付 Android、iOS、Web、桌面,渲染自绘、UI 一致性好。两个东西结合以后,最直接的收益就是:移动端团队现有的 Flutter 代码库不用推倒重来,只需要解决平台适配层,就能进入 OpenHarmony 生态。
我实际接触过的几个项目里,有做工业平板的、做智能座舱 HMI 的、做边缘计算网关管理界面的,都是先用 Flutter 写好界面和业务逻辑,再通过 OpenHarmony 的 Flutter 引擎适配层跑到底层系统上。选这条路的核心原因有两个:一是 Flutter 的布局和渲染在复杂界面下优势明显,二是 OpenHarmony 原生 ArkUI 虽然也在快速迭代,但如果团队没有 ArkTS 基础,学习成本并不比适配 Flutter 低。相比之下,直接沿用 Flutter 的工程习惯,只是把平台层换成 OpenHarmony,很多团队实测下来能省三分之一的改造时间。
1.2 OpenHarmony 版 Flutter 的目录设计思路
从架构角度理解,Flutter on OpenHarmony 并不是把 Flutter 的 Android 工程改个名,而是重新做了一层 ohos 平台适配。整个工程模型的逻辑是这样的:Dart 侧的 lib/ 仍然是我们熟悉的 Flutter 业务代码,而 ohos/ 目录就是替代原来 android/、ios/ 的 OpenHarmony 原生宿主工程。
这个设计的好处很明显:Flutter 工具链负责 Dart 编译和资源打包,OpenHarmony 的构建系统(hvigor)负责生成 HAP 应用包,两者通过生成代码和资源目录对接。所以在目录里你会看到很多长得像 Android Gradle 工程标记,但实际字段是 OpenHarmony 风格的配置文件,比如 build-profile.json5、hvigorfile.ts。如果你之前熟 Android 工程,这套思路很快就能迁移过来,只是要把 Gradle 的 Groovy 记忆收一收,换成 JSON5 和 TypeScript 写构建脚本。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 工程目录结构逐层拆解:从根目录到编译产物
2.1 根目录核心文件:Flutter 工程基因一眼识别
拿到任何一个 Flutter + OpenHarmony 工程,先看根目录。一个标准工程通常长这样:
code复制project_root/
├── android/
├── ios/
├── lib/
│ ├── main.dart
│ └── pages/
├── ohos/
│ ├── app/
│ ├── entry/
│ └── build-profile.json5
├── linux/
├── macos/
├── web/
├── windows/
├── pubspec.yaml
├── analysis_options.yaml
├── .metadata
└── flutter_ohos_config.yaml(部分分支会出现)
如果你在一个工程里看到了 ohos 目录,基本就可以判断这个工程是用带了 OpenHarmony 支持的 Flutter SDK 创建或迁移过的。根目录里最值得关注的文件按优先级排是这样:
pubspec.yaml:Dart 依赖管理,所有第三方包都在这声明,同时也是 Flutter 工具链识别工程类型的入口;analysis_options.yaml:代码静态检查规则,团队规范靠它统一;.metadata:Flutter 工具链生成的版本记录文件,注意别手动删,里面有迁移信息;ohos/:OpenHarmony 宿主工程所在,是整个工程中最核心的目录。
很多人拿到工程后会忽略 pubspec.yaml 里的 environment 字段,我建议先看一眼 sdk: ^3.x.x 和 flutter: ">=3. ...",因为 OpenHarmony 的 Flutter SDK 分支通常基于特定版本定制,版本不匹配会导致编译时出现大量无法解析的符号。后面第 3 章会展开说版本匹配的问题。
2.2 ohos 目录:OpenHarmony 宿主工程的五脏六腑
ohos 目录是整个工程的心脏。它的结构跟 DevEco Studio 创建的 Stage 模型工程保持一致,主要包括:
code复制ohos/
├── app/
│ ├── src/main/
│ │ ├── ets/
│ │ ├── resources/
│ │ └── module.json5
│ └── build-profile.json5
├── entry/
│ ├── src/main/
│ │ ├── ets/
│ │ │ ├── entryability/
│ │ │ ├── pages/
│ │ │ └── MainAbility.ets
│ │ ├── resources/
│ │ └── module.json5
│ ├── build-profile.json5
│ ├── hvigorfile.ts
│ └── obfuscation-rules.txt
├── build-profile.json5
├── hvigorfile.ts
└── hvigor-config.json5
这里面最容易看懵的是 app 和 entry 的区别。用生活化类比:app 相当于整套房子的结构,包括地基和公共水电;entry 相当于你实际住进去的那个房间,是应用启动入口和主要页面容器。OpenHarmony 的 Stage 模型里,一个 App 可以包含多个 entry(也就是多个模块/HAP),但大多数 Flutter 应用只需要一个 entry。
具体到每个文件:
ohos/build-profile.json5:整个工程的全局构建配置,声明了 SDK 版本、签名信息、产品配置;ohos/hvigorfile.ts:工程级 hvigor 构建脚本,负责拉起子模块构建;ohos/entry/build-profile.json5:entry 模块的构建配置,包含srcMain等路径配置,还有外部依赖(也就是 Flutter 引擎的 so 库)的链接方式;ohos/entry/hvigorfile.ts:模块级构建脚本,一般默认不用改;ohos/entry/src/main/module.json5:模块配置,声明了 Ability、权限、页面路由;ohos/entry/src/main/ets/:OpenHarmony 原生侧代码,入口 Ability 就在这里。
很多 Flutter 开发第一次打开这个目录,会打开 module.json5 一通改,结果编译直接报 module.json5: parse error。这里提醒一句:module.json5 是 JSON5 格式,允许注释和尾逗号,但字段名必须严格匹配系统要求。如果你不确定,先用 DevEco Studio 打开原工程模板对比,而不是手动瞎加字段。
2.3 entry 模块里的 ets 入口:Flutter 怎么和 OpenHarmony 握手
打开 ohos/entry/src/main/ets,你会看到类似这样的结构:
code复制ets/
├── entryability/
│ └── EntryAbility.ets
├── pages/
│ └── Index.ets
└── MainAbility.ets
EntryAbility.ets 是应用的入口 Ability,它的职责是:加载配置、设置窗口、然后创建 Flutter 引擎并挂载视图。这一段是 Flutter 和 OpenHarmony 握手的关键,简化后代码长这样:
typescript复制import { FlutterAbility, FlutterEngine } from '@ohos/flutter_ohos';
export default class EntryAbility extends FlutterAbility {
configureFlutterEngine(engine: FlutterEngine) {
super.configureFlutterEngine(engine);
// 这里可以注册 MethodChannel 的原生实现
engine.dartExecutor.setMessageHandler('native_channel', (call) => {
// 处理 Dart 侧发来的消息
});
}
}
这段代码里最值得关注的是 configureFlutterEngine 和 setMessageHandler,这是 Flutter 业务代码访问 OpenHarmony 系统能力的主要通道。比如你要读取设备序列号、调用 USB 管理、或者拿系统网络状态,都是一条 MethodChannel 的事。
在 pages/Index.ets 里,一般是创建 Flutter 视图容器:
typescript复制import { FlutterView } from '@ohos/flutter_ohos';
@Entry
@Component
struct Index {
build() {
Column() {
FlutterView({ engine: this.engine })
.width('100%')
.height('100%')
}
}
}
FlutterView 这个组件是 Flutter on OpenHarmony 适配层的核心封装,它接收一个 FlutterEngine 对象,把 Flutter 渲染出来的每一帧内容直接显示在 OpenHarmony 窗口里。你可以理解为 OpenHarmony 侧给 Flutter 提供了一个显存画布,Flutter 引擎把 UI 画上去,系统再上屏。
2.4 lib 目录与 Dart 业务代码的组织方式
从 Flutter 侧看,lib/ 目录的内容和平常在 Android/iOS 上开发没有什么区别。main.dart 是入口,内部会调用 runApp 启动整个 Flutter 应用。这里我强烈建议在 lib/ 下按功能模块分目录,而不是把所有页面堆在同一个文件里,因为 OpenHarmony 工程的编译速度本身比 Android 要慢一点,清晰的目录结构能让排查问题的时间大幅缩短。
一个我常用的组织方式:
code复制lib/
├── main.dart
├── pages/
│ ├── home_page.dart
│ └── settings_page.dart
├── services/
│ ├── device_service.dart
│ └── network_service.dart
├── utils/
│ └── logger.dart
└── widgets/
├── common_button.dart
└── empty_view.dart
Dart 侧和原生侧通信时,通常会在 services 里单独封装一个 channel 管理类,这样不会在页面代码里散落一堆 MethodChannel 调用。对于 OpenHarmony 适配来说,我见过不少工程把平台相关逻辑直接写进业务页面,后面要替换成别的平台就好痛苦。如果你打算长期维护 OpenHarmony 版,建议从第一天就把 channel 的协议收敛到一个独立模块。
3. 编译开发全流程实操:从零跑通 RK3568 与 RK3588
3.1 环境准备:版本匹配比什么都重要
有一说一,Flutter on OpenHarmony 目前最大的坑就是版本匹配。你可能已经下载了 Flutter SDK 3.16.9,但 OpenHarmony 的 Flutter 引擎只适配到某个特定 commit,结果编译时引擎源码接口对不上,报一堆符号找不到。这里给出一个当前实践中比较稳的组合建议:
| 组件 | 推荐版本说明 |
|---|---|
| Flutter SDK | 使用官方 OpenHarmony 分支(例如 flutter_flutter 的 feature/ohos 分支) |
| OpenHarmony SDK | 建议使用 4.0 Release 及以上版本,配套 API 9+ |
| DevEco Studio | 4.0 及以上版本,内置 hvigor 和 SDK 管理 |
| Node.js | hvigor 依赖,建议 16+ |
| 构建工具链 | 在 Ubuntu 20.04 以上环境编译,Windows 下需要装 DevEco Studio 配合 |
这里的核心逻辑是:Flutter SDK 分支决定了 Dart 版本和引擎源码,OpenHarmony SDK 决定了原生 API 的版本。两者之间通过 @ohos/flutter_ohos 这个 npm 包来桥接,这个包的版本你要是装错了,编译期必然报错。我踩过一次坑:Flutter 分支更新到较新版本后,@ohos/flutter_ohos 还停留在旧版本,导致 FlutterAbility 类的构造函数变了,连带 entryability 里一堆代码全要改。所以建议拿到工程后,先看 ohos/entry/oh-package.json5 里 @ohos/flutter_ohos 的版本,再去比对 Flutter 分支的 commit,保持两者同步更新。
3.2 创建工程并完成 OpenHarmony 适配
假设你手里已经有一个现成的 Flutter 工程,想把它适配到 OpenHarmony,步骤如下:
第一步,初始化 OpenHarmony 宿主目录。最省事的办法是用带 OpenHarmony 支持的 Flutter SDK 直接创建新工程:
bash复制flutter create --platforms=ohos my_app
如果 SDK 支持,会自动生成 ohos 目录。如果你是从老工程迁移,可以手动把另一个工程的 ohos 目录整体拷过来,然后修改包名和应用名。
第二步,修改 ohos/entry/src/main/module.json5。这里需要调整的关键字段是:
json5复制{
module: {
name: "entry",
type: "entry",
srcEntrance: "ets/entryability/EntryAbility.ets",
deviceTypes: ["tablet"],
abilities: [
{
name: "EntryAbility",
srcEntrance: "./ets/entryability/EntryAbility.ets",
enabled: true,
exported: true,
skills: [
{
entities: ["entity.system.home"],
actions: ["action.system.home"]
}
]
}
]
}
}
注意 deviceTypes 这个字段,如果你的应用要跑在 RK3568 平板上,写 ["tablet"];如果要跑在 RK3588 的盒子或者一体机上,可能要写 ["tv"] 或 ["tablet"]。这个字段直接关系应用能不能在设备上安装,很多人编译出 HAP 后往设备上装,却提示 install failed due to invalid device type,就是这里的类型和设备不匹配。
第三步,配置 build-profile.json5 里的签名信息。如果你是本地调试,可以用自动签名,DevEco Studio 登录后会自动生成。如果是在 CI 机器上编译,需要配置签名文件路径,否则打出的 HAP 安装到设备上会提示签名校验失败。
3.3 编译与运行:一条命令跑通全流程
环境准备好后,编译动作其实有两种方式。
方式一:用 DevEco Studio 打开工程根目录,等待同步完成后直接点运行。这种方式适合日常调试,因为 DevEco Studio 会帮你处理 SDK 路径、签名、hvigor 版本等一堆问题,对新手最友好。
方式二:纯命令行编译。在 ohos 目录下执行:
bash复制hvigorw assembleHap
如果提示找不到 hvigorw,说明工程没有拉取 hvigor 依赖,先执行:
bash复制ohpm install
hvigorw assembleHap
ohpm 是 OpenHarmony 的包管理器,类似 Flutter 的 pub。它负责安装 ohos/entry/oh-package.json5 里声明的依赖,包括 @ohos/flutter_ohos 和 @ohos/flutter_ohos_engine。
编译成功后,在 ohos/entry/build/default/outputs/default/ 目录下会生成 entry-default-signed.hap 或类似命名的安装包。然后通过 hdc 工具安装到设备:
bash复制hdc install 路径/entry-default-signed.hap
如果设备有两个网卡或调试口识别不到,先用 hdc list targets 查看设备连接情况。RK3568 的开发板经常出现 USB 转网口后连接不稳定,我建议用网口调试,hdc 支持 TCP 连接模式,只要开发板和电脑在同一局域网就能连上。
3.4 Flutter 引擎与资源打包的结构关系
编译完成后,你会发现在 entry/build 下除了 HAP 包,还有一堆中间产物。这里有一个常被忽略的点:Flutter 编译出来的 libflutter.so、libapp.so 以及 flutter_assets 目录,最终全部会打包进 HAP 的特定目录。
看一个典型 HAP 包解压后的结构:
code复制entry-default-signed.hap
├── module.json
├── resources/
├── libs/
│ └── arm64-v8a/
│ ├── libflutter.so
│ ├── libapp.so
│ └── libflutter_ohos.so
├── ets/
├── assets/
│ └── flutter_assets/
│ ├── AssetManifest.json
│ ├── kernel_blob.bin
│ └── fonts/
└── pack.info
注意 libapp.so 这个文件,它包含了编译后的 Dart AOT 产物,如果你的 Flutter 代码改了但没生效,先看这个文件是不是最新的。我之前遇到过一次诡异问题:Dart 代码改了,热重载显示成功,但杀掉 App 重新启动后还是旧界面。最后发现是 build-profile.json5 里配置了 release 模式的构建缓存,assembleHap 没有触发 Dart 重新编译,手动清了 build 目录再编就好了。
4. 常见问题与排查技巧实录
4.1 RK3568 多设备树到底怎么选
热搜词里出现 openharmony 的 rk3568 有许多设备树到底咋选,这个问题我太有共鸣了。OpenHarmony 内核设备树目录集中在 kernel/linux/linux-5.10/arch/arm64/boot/dts/rockchip/ 下,里面有一堆 .dts 文件,比如 rk3568-evb.dts、rk3568-nvr.dts、rk3568-iotest.dts,看起来每个都像能编译。
选设备树的核心逻辑是看你的硬件平台是什么。厂商提供的开发板如果基于官方 EVB 原理图,就直接选 rk3568-evb.dts;如果是厂商自研板子,通常会有一份自己的设备树补丁或单独 dts。判断方法很简单:先看板子上核心芯片、屏幕接口、触摸 IC 型号,再对比 dts 里的 chosen 节点和 i2c 节点挂载的设备,哪个匹配就用哪个。
我在实际项目中遇到过选了 rk3568-evb.dts 之后屏幕不亮、触摸没反应的案例,后来发现是板子的 LCD 接口走的是 mipi-dsi 而 EVB 板默认用的 edp。这种问题跟工程目录结构无关,但排查时需要在 ohos/entry/src/main/resources/rawfile 或产品配置里检查屏幕相关参数是否和设备树匹配。
还有一个小技巧:编译时可以在内核配置里指定要编译哪些设备树,不需要把 arch/arm64/boot/dts/rockchip/Makefile 下所有 dts 都编进去。减少编译时间的同时,也避免 u-boot 加载错误 dtb 导致启动黑屏。
4.2 依赖下载失败与 storage.flutter-io.cn 报错
另一个高频问题是 Flutter 依赖下载失败。报错信息通常长这样:
code复制Error: Unable to resolve dependency: Flutter assets will be downloaded from https://storage.flutter-io.cn
这个报错的意思是 Flutter 从 storage.flutter-io.cn 镜像下载引擎产物时失败了。原因一般是网络连接不通,或环境变量指向了错误的镜像。
排查步骤:
- 先看
PUB_HOSTED_URL和FLUTTER_STORAGE_BASE_URL这两个环境变量是否设置,如果设置了,确认地址还能访问; - 如果镜像地址失效,暂时取消环境变量,让 Flutter 走默认地址下载;
- 删除
~/.pub-cache下相关的缓存包,重新执行flutter pub get; - 检查
pubspec.lock里是否有锁定的旧版本依赖,如果和当前 SDK 不兼容,执行flutter pub upgrade。
也有一种情况是公司内网开了防火墙,导致常见域名不通。这种我就直接建议用 flutter 官方镜像或者在 pubspec.yaml 里将依赖源切到可用镜像源,但注意不要写死内网地址到交付工程里,否则换环境就编译不过。
4.3 Gradle 主插件报错和 CMake 编译问题
你可能会好奇,Flutter 工程关 Gradle 什么事?这是因为 OpenHarmony 的 Flutter SDK 在构建时,有部分源码还需要通过交叉编译生成引擎产物。如果你用的是从 Flutter 官方 Android 分支改过来的 SDK,可能连带要做一点 Gradle 编译工作。最常见的报错是:
code复制You are applying Flutter's main Gradle plugin imperatively using the apply script method, ...
这个报错是在提醒你 Flutter 的 Gradle 插件应用方式已经改为 declarative 方式(使用 plugins {} 块),不能再用老式的 apply 方法。排查方法:打开 android/settings.gradle 或根 build.gradle,把 apply from: "$flutterRoot/packages/flutter_tools/gradle/app_plugin_loader.gradle" 这类老写法改成新版 Flutter 工具链要求的声明式写法,或直接升级 Flutter SDK 版本。
CMake 报错也比较常见,典型的是:
code复制CMake Error at CMakeLists.txt:3 (project): Generator Visual Studio 16 2019 ... does not support ...
出现在 Windows 上编译 Flutter 插件时,核心原因是 Visual Studio 没有装对应版本的 C++ 工具链。打开 Visual Studio Installer,安装“使用 C++ 的桌面开发”工作负载,并确认 CMake 和 Windows SDK 版本匹配即可。如果你只跑 OpenHarmony 的远程交叉编译,也可能直接跳过本地 CMake,但不同 SDK 分支行为不一样,遇到这个问题时优先走 DevEco Studio 的构建链路而不是 flutter build 原生命令。
4.4 热重载不生效与调试建议
Flutter 开发离不开热重载,但在 OpenHarmony 设备上,热重载的表现并不稳定。我实测下来,RK3568 上通过 USB 连接时热重载成功率在 80% 左右,但 RK3588 使用网口调试时成功率会更高一点。原因大概率是 USB 节点的并发稳定性问题,不一定是 Flutter 引擎的锅。
如果你发现按 r 键没有触发热重载,可以先执行:
bash复制flutter attach -d 设备ID --debug-port 12345
手动连接后,再截图、日志、性能分析都会正常。如果 attach 不上去,八成是设备端 Flutter 引擎启动的时候没有开启 VM Service,或者端口被占用。
调试建议方面,OpenHarmony 侧日志用 hdc hilog 查看,Dart 侧日志用 flutter logs 或 debugPrint 输出,这两个通道是分开的。当你需要排查 channel 通信问题时,两边日志都要看,只在 Dart 侧打日志很容易漏掉原生侧抛出的异常。
5. 写在最后:先把目录读懂,再动手改代码
个人经验来看,Flutter on OpenHarmony 的学习曲线其实并不陡,真正让人摔跟头的都是工程结构层面的细节。读懂了 ohos 目录里每个文件的作用,编译报错时你能快速判断是哪个环节出了问题,而不是把整个工程删了重新拉。我个人在实际排查问题的过程中体会很深的一点是:遇到搞不懂的配置,先不要直接改,打开 DevEco Studio 里新建一个同版本 Flutter + OpenHarmony 模板工程,两边 diff 一下,很多时候答案就出来了。尤其是 module.json5 和 build-profile.json5 这类文件,模板工程就是最好的参考文档。
最后再分享一个小技巧:在 ohos 目录下开发时,可以把常用命令写成一个 shell 脚本,比如先 hdc list targets 确认设备在线,再 hvigorw assembleHap 编译,最后 hdc install 安装并启动。这套流程跑顺以后,从改代码到上真机验证,一分钟内搞定,比来回切换 DevEco Studio 点按钮要快得多。等 OpenHarmony 生态的 Flutter 适配进一步完善后,这个流程只会变得更简单,但工程目录结构这块基本功,无论工具怎么变都是绕不开的。
