前言
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net


Flutter插件的注册和生命周期管理 是确保插件正常工作的关键环节,它决定了插件何时被创建、何时开始接收消息以及何时被销毁。在OpenHarmony平台上,插件注册流程与Android和iOS平台有所不同,但核心思想是一致的------通过框架提供的注册机制 将原生插件与Flutter引擎绑定在一起。本文将结合 apple_product_name 库的真实源码,详细介绍插件从创建到销毁的完整过程。
提示:本文所有源码均来自 apple_product_name 仓库,建议对照源码阅读以获得最佳学习效果。
一、插件注册概述
1.1 注册流程总览
插件注册是Flutter框架在应用启动阶段自动完成的一个关键过程。整个流程可以概括为以下步骤:
- 应用启动,系统创建
EntryAbility实例 EntryAbility.configureFlutterEngine()被调用- 内部调用
GeneratedPluginRegistrant.registerWith() - 遍历所有插件,依次调用
new XxxPlugin()创建实例 - 框架调用每个插件的
onAttachedToEngine()完成初始化 - 插件进入正常工作状态,开始处理 Dart 端的方法调用
1.2 注册时机
注册发生在引擎配置阶段------此时引擎已完成基本初始化但尚未开始运行 Dart 代码,这是注册插件的最佳时机。注册过程是同步执行的,如果某个插件的注册耗时过长,可能会影响应用的启动速度。
| 阶段 | 时机 | 引擎状态 | 插件状态 |
|---|---|---|---|
| 引擎创建 | 应用启动 | 初始化中 | 未创建 |
| 引擎配置 | configureFlutterEngine | 基本就绪 | 注册中 |
| Dart 启动 | runApp() | 完全就绪 | 工作中 |
| 引擎销毁 | 应用退出 | 销毁中 | 分离中 |
注意:开发者无需手动干预注册时机,只需确保插件在
GeneratedPluginRegistrant中被正确声明即可。详见 Flutter 平台通道文档。
二、GeneratedPluginRegistrant 文件解析
2.1 真实源码
以下是项目中 GeneratedPluginRegistrant.ets 的完整源码:
typescript
// example/ohos/entry/src/main/ets/plugins/GeneratedPluginRegistrant.ets
import { FlutterEngine, Log } from '@ohos/flutter_ohos';
import AppleProductNamePlugin from 'apple_product_name';
const TAG = "GeneratedPluginRegistrant";
export class GeneratedPluginRegistrant {
static registerWith(flutterEngine: FlutterEngine) {
try {
flutterEngine.getPlugins()?.add(new AppleProductNamePlugin());
} catch (e) {
Log.e(
TAG,
"Tried to register plugins with FlutterEngine ("
+ flutterEngine
+ ") failed.");
Log.e(TAG, "Received exception while registering", e);
}
}
}
2.2 逐行解析
这个文件虽然只有短短十几行,但每一行都有明确的职责:
| 行号 | 代码 | 作用 |
|---|---|---|
| 1 | import { FlutterEngine, Log } |
从 Flutter 引擎库导入核心类 |
| 2 | import AppleProductNamePlugin |
从插件包导入插件类 |
| 4 | const TAG = "GeneratedPluginRegistrant" |
定义日志标签,便于过滤 |
| 8 | flutterEngine.getPlugins()?.add(...) |
获取插件注册表并添加插件实例 |
| 9-13 | catch (e) { Log.e(...) } |
捕获注册异常,记录错误日志 |
2.3 关键设计要点
这个文件有几个值得注意的设计细节:
- 使用
?.可选链操作符 调用getPlugins(),防止引擎未就绪时空指针 - try-catch 包裹整个注册过程,单个插件注册失败不会导致应用崩溃
- 使用
Log.e()而非console.log()记录错误,符合 OpenHarmony 的日志规范 - 文件头部注释标注 "Generated file. Do not edit",提示这是自动生成的文件
提示:在 OpenHarmony 适配场景下,这个文件可能需要手动维护 。每当项目中添加或移除 Flutter 插件时,都需要相应更新此文件。详见 OpenHarmony Flutter 适配指南。
三、EntryAbility 中的注册调用
3.1 真实源码
以下是项目中 EntryAbility.ets 的完整源码:
typescript
// example/ohos/entry/src/main/ets/entryability/EntryAbility.ets
import { FlutterAbility, FlutterEngine } from '@ohos/flutter_ohos';
import { GeneratedPluginRegistrant } from '../plugins/GeneratedPluginRegistrant';
export default class EntryAbility extends FlutterAbility {
configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
GeneratedPluginRegistrant.registerWith(flutterEngine)
}
}
3.2 调用链路
应用启动
→ 系统创建 EntryAbility
→ configureFlutterEngine(flutterEngine)
→ super.configureFlutterEngine(flutterEngine) // 父类基础配置
→ GeneratedPluginRegistrant.registerWith(flutterEngine) // 插件注册
→ flutterEngine.getPlugins()?.add(new AppleProductNamePlugin())
→ AppleProductNamePlugin.onAttachedToEngine(binding)
3.3 调用顺序的重要性
super.configureFlutterEngine 必须在 registerWith 之前调用,原因如下:
- 父类方法完成
BinaryMessenger的初始化 - 父类方法设置 Dart 入口点
- 插件注册依赖已初始化的
BinaryMessenger来创建MethodChannel
如果颠倒顺序,binding.getBinaryMessenger() 可能返回未就绪的对象,导致 MethodChannel 创建失败。
注意:
EntryAbility继承自 FlutterAbility,这是 OpenHarmony 平台的 Flutter 容器基类,类似于 Android 的FlutterActivity。
四、插件生命周期四阶段
4.1 阶段概览
插件的生命周期分为四个明确的阶段,每个阶段都有特定的职责:
| 阶段 | 触发方法 | 职责 | 对应源码位置 |
|---|---|---|---|
| 创建 | constructor() |
初始化成员变量 | AppleProductNamePlugin.ets 第78行 |
| 附加 | onAttachedToEngine() |
创建 MethodChannel、设置处理器 | 第84行 |
| 运行 | onMethodCall() |
处理 Dart 端方法调用 | 第93行 |
| 分离 | onDetachedFromEngine() |
释放资源、清理引用 | 第88行 |
4.2 生命周期流转
constructor() → onAttachedToEngine() → onMethodCall() × N → onDetachedFromEngine()
创建 附加 运行(多次) 分离
每个阶段的转换都由 Flutter 框架驱动 ,开发者需要在对应的回调方法中实现正确的逻辑。特别是在分离阶段,如果资源没有被正确释放,可能会导致内存泄漏甚至应用崩溃。
五、构造函数实现
5.1 真实源码
typescript
export default class AppleProductNamePlugin implements FlutterPlugin, MethodCallHandler {
private channel: MethodChannel | null = null;
constructor() {
}
}
5.2 设计原则
apple_product_name 的构造函数是空实现,这是一种刻意的设计选择:
- 将
channel初始化为null,因为此时BinaryMessenger尚未就绪 - 不进行任何复杂操作或资源分配
- 所有依赖引擎资源的初始化推迟到
onAttachedToEngine中
5.3 为什么构造函数要保持简单
构造函数保持简单有三个重要原因:
- 避免空指针 :构造时引擎资源未就绪,访问
BinaryMessenger会失败 - 支持多引擎 :每个引擎实例提供独立的
BinaryMessenger,不能在构造时绑定 - 快速启动:构造函数在注册阶段同步执行,耗时过长会阻塞应用启动
提示:这种延迟初始化 模式是 Flutter 插件开发的通用最佳实践,不仅适用于 OpenHarmony 平台,在 Android 和 iOS 平台同样适用。
六、附加阶段详解(onAttachedToEngine)
6.1 真实源码
typescript
onAttachedToEngine(binding: FlutterPluginBinding): void {
this.channel = new MethodChannel(binding.getBinaryMessenger(), "apple_product_name");
this.channel.setMethodCallHandler(this);
}
6.2 两行代码的深层含义
这两行代码完成了插件初始化的全部工作:
typescript
// 第一行:创建 MethodChannel
this.channel = new MethodChannel(
binding.getBinaryMessenger(), // 获取引擎提供的消息传递器
"apple_product_name" // 通道名称,必须与 Dart 端一致
);
// 第二行:设置消息处理器
this.channel.setMethodCallHandler(this); // this 实现了 MethodCallHandler 接口
6.3 通道名称匹配
通道名称 "apple_product_name" 必须与 Dart 端定义的完全一致:
dart
// lib/apple_product_name_ohos.dart
class OhosProductName {
static const MethodChannel _channel = MethodChannel('apple_product_name');
// ^^^^^^^^^^^^^^^^^^^^^^^^
// 必须与原生端完全一致
}
| 端 | 文件 | 通道名称 |
|---|---|---|
| Dart | lib/apple_product_name_ohos.dart |
'apple_product_name' |
| 原生 | AppleProductNamePlugin.ets |
"apple_product_name" |
如果两端名称不一致,Dart 端调用 invokeMethod 时会抛出 MissingPluginException。
注意:通道名称是大小写敏感 的,
"Apple_Product_Name"和"apple_product_name"是两个不同的通道。建议使用全小写加下划线的命名风格。
七、分离阶段详解(onDetachedFromEngine)
7.1 真实源码
typescript
onDetachedFromEngine(binding: FlutterPluginBinding): void {
if (this.channel != null) {
this.channel.setMethodCallHandler(null);
this.channel = null;
}
}
7.2 资源释放三步骤
分离阶段的代码虽然简短,但严格遵循了三步释放的顺序:
- 移除消息处理器 :
this.channel.setMethodCallHandler(null)--- 停止接收新消息 - 清理引用 :
this.channel = null--- 帮助 GC 回收 MethodChannel 对象 - 空值保护 :
if (this.channel != null)--- 防止重复调用导致空指针
7.3 为什么顺序很重要
typescript
// ✓ 正确顺序:先停止接收消息,再清理引用
this.channel.setMethodCallHandler(null); // 步骤1:不再接收新消息
this.channel = null; // 步骤2:释放引用
// ✗ 错误顺序:先清理引用会导致无法移除处理器
this.channel = null; // channel 已为 null
this.channel.setMethodCallHandler(null); // 空指针异常!
如果颠倒顺序,channel 被置为 null 后就无法再调用 setMethodCallHandler(null),消息处理器将永远不会被移除,可能导致内存泄漏。
提示:
apple_product_name插件没有数据库连接、网络请求等额外资源需要释放,因此分离逻辑非常简洁。如果你的插件持有其他资源,应在移除处理器之后、清理引用之前释放它们。
八、运行阶段详解(onMethodCall)
8.1 真实源码
typescript
onMethodCall(call: MethodCall, result: MethodResult): void {
switch (call.method) {
case "getMachineId":
this.getMachineId(result);
break;
case "getProductName":
this.getProductName(result);
break;
case "lookup":
this.lookup(call, result);
break;
default:
result.notImplemented();
break;
}
}
8.2 方法路由表
onMethodCall 是插件的消息路由器,根据方法名分发到对应的处理函数:
| Dart 端调用 | call.method | 原生处理方法 | 参数 |
|---|---|---|---|
getMachineId() |
"getMachineId" |
this.getMachineId(result) |
无 |
getProductName() |
"getProductName" |
this.getProductName(result) |
无 |
lookup(id) |
"lookup" |
this.lookup(call, result) |
machineId |
8.3 default 分支的作用
typescript
default:
result.notImplemented();
break;
当 Dart 端调用了一个原生侧未实现的方法时,result.notImplemented() 会在 Dart 端抛出 MissingPluginException。这是 Flutter 插件的标准约定,确保调用方能明确知道某个方法不被支持。
九、插件导出入口
9.1 index.ets 源码
typescript
// ohos/index.ets
import AppleProductNamePlugin from './src/main/ets/components/plugin/AppleProductNamePlugin';
export default AppleProductNamePlugin;
export { AppleProductNamePlugin };
9.2 导出链路
GeneratedPluginRegistrant.ets
→ import AppleProductNamePlugin from 'apple_product_name'
→ oh-package.json5 中 main: "index.ets"
→ ohos/index.ets
→ export default AppleProductNamePlugin
→ ohos/src/main/ets/components/plugin/AppleProductNamePlugin.ets
9.3 双重导出的原因
index.ets 同时使用了 default export 和 named export:
typescript
export default AppleProductNamePlugin; // 支持 import Plugin from 'xxx'
export { AppleProductNamePlugin }; // 支持 import { AppleProductNamePlugin } from 'xxx'
这种双重导出方式兼容了两种导入风格,提高了模块的易用性。
提示:
oh-package.json5中的"main": "index.ets"字段指定了模块入口文件,详见本系列第22篇:oh-package配置详解。
十、getUniqueClassName 方法
10.1 源码
typescript
getUniqueClassName(): string {
return TAG; // 返回 "AppleProductNamePlugin"
}
10.2 作用说明
getUniqueClassName 是 FlutterPlugin 接口要求实现的方法,它返回插件的唯一标识符。Flutter 框架使用这个标识符来:
- 防止同一个插件被重复注册
- 在日志中标识插件来源
- 在插件注册表中作为查找键
10.3 命名约定
| 插件 | getUniqueClassName 返回值 | TAG 常量 |
|---|---|---|
| apple_product_name | "AppleProductNamePlugin" | 与类名一致 |
建议 getUniqueClassName 返回值与类名保持一致,使用 PascalCase 命名风格。apple_product_name 通过复用 TAG 常量实现了这一点,同时保证了日志标签和唯一标识的一致性。
十一、FlutterPlugin 接口
11.1 接口定义
AppleProductNamePlugin 实现了两个核心接口:
typescript
export default class AppleProductNamePlugin
implements FlutterPlugin, MethodCallHandler {
// ...
}
11.2 接口职责对比
| 接口 | 来源 | 核心方法 | 职责 |
|---|---|---|---|
| FlutterPlugin | @ohos/flutter_ohos |
onAttachedToEngine, onDetachedFromEngine, getUniqueClassName |
管理插件生命周期 |
| MethodCallHandler | @ohos/flutter_ohos |
onMethodCall |
处理方法调用 |
11.3 为什么分成两个接口
将生命周期管理和消息处理分成两个接口,体现了单一职责原则:
FlutterPlugin关注插件与引擎的绑定关系MethodCallHandler关注具体的业务逻辑处理
这种设计使得一个插件可以将消息处理委托给其他对象,而不必自己实现所有逻辑。apple_product_name 选择让同一个类实现两个接口(this.channel.setMethodCallHandler(this)),因为插件逻辑足够简单,不需要额外的委托。
提示:
FlutterPlugin和MethodCallHandler都来自 @ohos/flutter_ohos 包,这是 OpenHarmony 平台的 Flutter 引擎运行时库。
十二、多引擎场景
12.1 多引擎的工作方式
在混合开发模式下,一个应用可能同时运行多个 Flutter 引擎实例。每个引擎都会独立创建插件实例:
typescript
// 引擎 A 创建的插件实例
const pluginA = new AppleProductNamePlugin();
pluginA.onAttachedToEngine(bindingA); // 使用引擎 A 的 BinaryMessenger
// 引擎 B 创建的插件实例
const pluginB = new AppleProductNamePlugin();
pluginB.onAttachedToEngine(bindingB); // 使用引擎 B 的 BinaryMessenger
12.2 实例变量 vs 静态变量
apple_product_name 使用实例变量 存储 channel,这是支持多引擎的关键:
typescript
export default class AppleProductNamePlugin {
// ✓ 实例变量:每个实例独立
private channel: MethodChannel | null = null;
// ✗ 如果用静态变量:所有实例共享,会导致冲突
// private static channel: MethodChannel | null = null;
}
使用静态变量的风险:
- 引擎 B 的注册会覆盖引擎 A 的 channel
- 引擎 A 的 Dart 端调用会被路由到引擎 B
- 引擎 A 分离时会误清理引擎 B 的 channel
十三、热重载与插件状态
13.1 热重载对插件的影响
Flutter 热重载时,Dart 代码会被重新加载,但原生插件实例不会被销毁和重新创建。这意味着:
constructor()不会被再次调用onAttachedToEngine()不会被再次调用onDetachedFromEngine()不会被调用- 插件实例的内存状态保持不变
13.2 apple_product_name 的天然兼容
apple_product_name 插件天然兼容热重载,因为:
- 插件不持有任何可变状态(没有缓存、没有计数器)
- 每次
onMethodCall都是无状态的------直接读取系统 API 或查询映射表 deviceInfo.productModel和HUAWEI_DEVICE_MAP都是不变数据
13.3 需要注意热重载的场景
如果你的插件持有可变状态,需要考虑热重载的影响:
typescript
// 有状态的插件需要防御性编程
onAttachedToEngine(binding: FlutterPluginBinding): void {
if (this.channel != null) {
// 热重载场景:channel 已存在,跳过重复初始化
return;
}
this.channel = new MethodChannel(binding.getBinaryMessenger(), "xxx");
this.channel.setMethodCallHandler(this);
}
注意:
apple_product_name的源码中没有这个防护,因为 OpenHarmony 平台的 Flutter 框架保证了onAttachedToEngine在热重载时不会被重复调用。但在编写自己的插件时,添加这个防护是一种安全的做法。
十四、错误处理机制
14.1 注册阶段的错误处理
GeneratedPluginRegistrant 中的 try-catch 确保单个插件注册失败不会影响其他插件:
typescript
static registerWith(flutterEngine: FlutterEngine) {
try {
flutterEngine.getPlugins()?.add(new AppleProductNamePlugin());
} catch (e) {
Log.e(TAG, "Tried to register plugins with FlutterEngine ("
+ flutterEngine + ") failed.");
Log.e(TAG, "Received exception while registering", e);
}
}
14.2 运行阶段的错误处理
每个业务方法内部都有独立的 try-catch:
typescript
private getMachineId(result: MethodResult): void {
try {
result.success(deviceInfo.productModel);
} catch (e) {
const errorMsg = e instanceof Error ? e.message : String(e);
result.error("GET_MACHINE_ID_ERROR", errorMsg, null);
}
}
14.3 错误传递链路
原生异常 → catch(e) → result.error(code, message, details)
→ MethodChannel 传递
→ Dart 端 PlatformException(code, message, details)
→ 业务代码 try-catch 捕获
错误码遵循大写下划线命名规范:
GET_MACHINE_ID_ERROR--- 获取型号标识符失败GET_PRODUCT_NAME_ERROR--- 获取产品名称失败LOOKUP_ERROR--- 查询映射表失败INVALID_ARGUMENT--- 参数缺失或无效
十五、日志记录最佳实践
15.1 TAG 常量
typescript
const TAG = "AppleProductNamePlugin";
apple_product_name 在文件顶部定义了 TAG 常量,这个常量被复用于:
getUniqueClassName()的返回值GeneratedPluginRegistrant中的日志标签- 错误日志的前缀标识
15.2 日志级别建议
| 场景 | 日志级别 | 方法 | 示例 |
|---|---|---|---|
| 生命周期事件 | Debug | Log.d(TAG, ...) |
"onAttachedToEngine called" |
| 方法调用 | Info | Log.i(TAG, ...) |
"Method called: getMachineId" |
| 注册失败 | Error | Log.e(TAG, ...) |
"Tried to register plugins failed" |
| 业务异常 | Error | Log.e(TAG, ...) |
"getMachineId error: ..." |
15.3 在 DevEco Studio 中过滤日志
在 DevEco Studio 的 Log 窗口中,可以使用 TAG 过滤插件相关日志:
过滤条件: AppleProductNamePlugin
这样可以在大量系统日志中快速定位到插件的运行信息,大幅提升调试效率。
提示:生产环境中建议通过编译开关控制日志输出量,避免敏感信息泄露和性能开销。详见 OpenHarmony 日志管理文档。
十六、与 Android/iOS 插件注册的对比
16.1 跨平台对比
| 维度 | OpenHarmony | Android | iOS |
|---|---|---|---|
| 入口文件 | EntryAbility.ets |
MainActivity.kt |
AppDelegate.swift |
| 注册文件 | GeneratedPluginRegistrant.ets |
GeneratedPluginRegistrant.java |
GeneratedPluginRegistrant.m |
| 插件基类 | FlutterPlugin |
FlutterPlugin |
FlutterPlugin |
| 语言 | ArkTS | Kotlin/Java | Swift/ObjC |
| 生成方式 | 手动/半自动 | 自动生成 | 自动生成 |
16.2 核心差异
OpenHarmony 平台与 Android/iOS 的主要差异:
- 注册文件维护方式 :Android/iOS 由
flutter pub get自动生成,OpenHarmony 可能需要手动维护 - 容器基类 :OpenHarmony 使用
FlutterAbility(对应 Android 的FlutterActivity) - 消息传递 :底层都使用
BinaryMessenger,但 OpenHarmony 的实现基于 ArkTS 运行时
16.3 生命周期方法一致性
三个平台的插件生命周期方法完全一致:
onAttachedToEngine(binding) --- 三平台统一
onMethodCall(call, result) --- 三平台统一
onDetachedFromEngine(binding) --- 三平台统一
这种一致性使得有 Android 或 iOS 插件开发经验的开发者可以快速上手 OpenHarmony 插件开发。
十七、完整注册链路图
17.1 从 Dart 到原生的完整链路
dart
// Dart 端
final name = await OhosProductName().getProductName();
OhosProductName().getProductName()
→ _channel.invokeMethod('getProductName')
→ MethodChannel('apple_product_name')
→ BinaryMessenger 编码消息
→ 跨平台传递
→ AppleProductNamePlugin.onMethodCall(call, result)
→ case "getProductName": this.getProductName(result)
→ deviceInfo.productModel → HUAWEI_DEVICE_MAP 查找
→ result.success(productName)
→ BinaryMessenger 编码响应
→ 跨平台传递
→ Future<String> 完成
17.2 涉及的文件清单
完整的注册和调用链路涉及以下文件:
| 文件 | 语言 | 职责 |
|---|---|---|
lib/apple_product_name_ohos.dart |
Dart | Dart 端 API 封装 |
EntryAbility.ets |
ArkTS | 应用入口,触发插件注册 |
GeneratedPluginRegistrant.ets |
ArkTS | 插件注册表 |
ohos/index.ets |
ArkTS | 模块导出入口 |
AppleProductNamePlugin.ets |
ArkTS | 插件核心实现 |
十八、常见问题排查
18.1 MissingPluginException
现象 :Dart 端调用时抛出 MissingPluginException
排查步骤:
- 检查
GeneratedPluginRegistrant.ets是否导入了插件 - 检查
EntryAbility是否调用了registerWith - 检查通道名称是否两端一致(
"apple_product_name") - 清理构建缓存后重新编译
18.2 插件注册但方法调用无响应
现象:不抛异常但 Future 永远不完成
排查步骤:
- 检查
onMethodCall中的 switch-case 是否覆盖了该方法名 - 检查是否每个分支都调用了
result.success()或result.error() - 检查
default分支是否调用了result.notImplemented()
18.3 快速诊断代码
dart
Future<void> diagnosePlugin() async {
try {
final ohos = OhosProductName();
final id = await ohos.getMachineId();
print('✓ 插件注册正常,machineId: $id');
} on MissingPluginException {
print('✗ 插件未注册,请检查 GeneratedPluginRegistrant');
} on PlatformException catch (e) {
print('✗ 原生异常: ${e.code} - ${e.message}');
} catch (e) {
print('✗ 未知错误: $e');
}
}
提示:更多调试技巧请参考本系列第29篇:调试技巧与常见问题排查。
十九、最佳实践清单
19.1 构造函数
- 保持空实现或仅初始化基本变量
- 不访问引擎资源,不创建 MethodChannel
- 不执行耗时操作
19.2 onAttachedToEngine
- 创建 MethodChannel 并设置处理器
- 通道名称与 Dart 端严格一致
- 添加异常处理防止注册失败影响应用
19.3 onMethodCall
- 使用 switch-case 路由方法调用
- 每个分支必须调用 result 的某个方法(success/error/notImplemented)
- default 分支调用
result.notImplemented()
19.4 onDetachedFromEngine
- 先移除处理器,再清理引用
- 添加空值检查防止重复调用
- 释放所有在 onAttachedToEngine 中创建的资源
总结
插件注册与生命周期管理是 Flutter 插件开发的基石。apple_product_name 通过简洁的实现展示了一个标准的插件生命周期:空构造函数 → onAttachedToEngine 创建通道 → onMethodCall 处理调用 → onDetachedFromEngine 释放资源。理解这个流程,是开发自己的 OpenHarmony Flutter 插件的第一步。
下一篇文章将介绍 oh-package.json5 配置详解。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
- OpenHarmony适配仓库:apple_product_name
- 开源鸿蒙跨平台社区:openharmonycrossplatform.csdn.net
- Flutter 平台通道文档:flutter.dev
- Flutter 插件开发指南:flutter.dev
- OpenHarmony Flutter 引擎:gitee.com