前言
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
上一篇我们搭好了OpenHarmony插件的工程骨架,今天开始往里面填肉------实现FlutterPlugin接口。这是整个适配工作中最核心的部分之一,搞定了这个接口,插件就能被Flutter引擎正确加载和管理了。
我在实现这个接口的时候,最大的感受是:Flutter-OHOS团队做了一件非常聪明的事------他们把OpenHarmony的FlutterPlugin接口设计得和Android端几乎一模一样。如果你之前写过Android的Flutter插件,转到OpenHarmony基本上是"换个语法重写一遍"的事。
当然,"几乎一模一样"不等于"完全一样"。ArkTS和Java毕竟是不同的语言,有些地方的写法差异还是挺大的。今天我就把这些差异一一讲清楚。
💡 本文重点 :FlutterPlugin接口的三个核心方法------
getUniqueClassName、onAttachedToEngine、onDetachedFromEngine。
一、@ohos/flutter_ohos 框架中的 FlutterPlugin 接口
1.1 框架导入
OpenHarmony的Flutter插件框架通过@ohos/flutter_ohos包提供:
typescript
import {
FlutterPlugin,
FlutterPluginBinding,
MethodCall,
MethodCallHandler,
MethodChannel,
MethodResult,
AbilityAware,
} from '@ohos/flutter_ohos';
这一行import语句包含了插件开发需要的所有核心类型。我们逐个看:
| 类型 | 职责 | Android对应 |
|---|---|---|
| FlutterPlugin | 插件生命周期接口 | io.flutter.embedding.engine.plugins.FlutterPlugin |
| FlutterPluginBinding | 引擎绑定信息 | FlutterPlugin.FlutterPluginBinding |
| MethodCall | 方法调用封装 | io.flutter.plugin.common.MethodCall |
| MethodCallHandler | 方法调用处理器 | MethodChannel.MethodCallHandler |
| MethodChannel | 通信通道 | io.flutter.plugin.common.MethodChannel |
| MethodResult | 方法调用结果 | MethodChannel.Result |
| AbilityAware | Ability生命周期感知 | ActivityAware |
1.2 FlutterPlugin 接口定义
FlutterPlugin接口定义了插件的生命周期方法:
typescript
interface FlutterPlugin {
// 获取插件唯一标识
getUniqueClassName(): string;
// 插件附加到Flutter引擎
onAttachedToEngine(binding: FlutterPluginBinding): void;
// 插件从Flutter引擎分离
onDetachedFromEngine(binding: FlutterPluginBinding): void;
}
对比Android端:
java
// Android的FlutterPlugin接口
public interface FlutterPlugin {
void onAttachedToEngine(FlutterPluginBinding binding);
void onDetachedFromEngine(FlutterPluginBinding binding);
}
📌 差异点 :OpenHarmony多了一个
getUniqueClassName()方法,Android没有。这个方法用于插件的唯一标识,避免重复注册。
1.3 MethodCallHandler 接口
typescript
interface MethodCallHandler {
onMethodCall(call: MethodCall, result: MethodResult): void;
}
这个接口只有一个方法,负责处理所有来自Dart层的方法调用。和Android端完全一致。
二、FlutterPluginBinding 与 BinaryMessenger
2.1 FlutterPluginBinding 的作用
FlutterPluginBinding是Flutter引擎传给插件的绑定对象,包含了插件运行所需的各种资源:
typescript
// FlutterPluginBinding提供的核心方法
interface FlutterPluginBinding {
getBinaryMessenger(): BinaryMessenger; // 获取消息传递器
getApplicationContext(): Context; // 获取应用上下文
// ... 其他方法
}
2.2 BinaryMessenger
BinaryMessenger是Platform Channel的底层通信接口,MethodChannel就是基于它构建的:
typescript
// 通过BinaryMessenger创建MethodChannel
onAttachedToEngine(binding: FlutterPluginBinding): void {
this.channel = new MethodChannel(
binding.getBinaryMessenger(), // BinaryMessenger
"com.flutter.speech_recognition" // Channel名称
);
this.channel.setMethodCallHandler(this); // 注册方法处理器
}
这段代码做了三件事:
- 从
binding中获取BinaryMessenger - 用它创建
MethodChannel,指定Channel名称 - 将当前插件实例注册为方法处理器
🎯 Channel名称必须和Dart端一致 :Dart端定义的是
'com.flutter.speech_recognition',这里也必须用完全相同的字符串。差一个字符都不行。
2.3 完整的绑定流程
Flutter Engine启动
│
├── 创建FlutterPluginBinding
│ ├── 初始化BinaryMessenger
│ └── 准备ApplicationContext
│
├── 扫描已注册的插件
│ └── 找到FlutterSpeechPlugin
│
└── 调用plugin.onAttachedToEngine(binding)
├── 创建MethodChannel
├── 注册MethodCallHandler
└── 插件就绪,等待Dart端调用
三、onAttachedToEngine / onDetachedFromEngine 生命周期
3.1 onAttachedToEngine 实现
这是插件初始化的入口,flutter_speech的实现:
typescript
onAttachedToEngine(binding: FlutterPluginBinding): void {
this.channel = new MethodChannel(
binding.getBinaryMessenger(),
"com.flutter.speech_recognition"
);
this.channel.setMethodCallHandler(this);
}
在这个方法里应该做什么:
- ✅ 创建MethodChannel
- ✅ 注册MethodCallHandler
- ✅ 初始化必要的成员变量
- ❌ 不要做耗时操作(会阻塞引擎启动)
- ❌ 不要申请权限(此时还没有Ability上下文)
- ❌ 不要创建语音识别引擎(等用户调用activate时再创建)
⚠️ 重要 :
onAttachedToEngine在引擎启动时调用,此时Ability可能还没有准备好。所以不能在这里做需要UIAbilityContext的操作(比如权限申请)。权限申请要等到onAttachedToAbility之后。
3.2 onDetachedFromEngine 实现
这是插件销毁的出口:
typescript
onDetachedFromEngine(binding: FlutterPluginBinding): void {
if (this.channel != null) {
this.channel.setMethodCallHandler(null);
}
this.destroyEngine();
}
在这个方法里应该做什么:
- ✅ 取消MethodCallHandler注册
- ✅ 销毁语音识别引擎
- ✅ 释放所有资源
- ✅ 置空所有引用
3.3 生命周期时序图
App启动
│
├── FlutterEngine创建
│ │
│ ├── onAttachedToEngine() ← 创建Channel
│ │ 时机:引擎启动时
│ │ 可用资源:BinaryMessenger
│ │
│ ├── onAttachedToAbility() ← 获取Context(下一篇讲)
│ │ 时机:Ability启动后
│ │ 可用资源:UIAbilityContext
│ │
│ ├── [正常运行阶段]
│ │ 处理Dart方法调用
│ │ 发送回调到Dart层
│ │
│ ├── onDetachedFromAbility() ← 释放Context
│ │ 时机:Ability销毁前
│ │
│ └── onDetachedFromEngine() ← 清理所有资源
│ 时机:引擎销毁时
│
└── App退出
💡 记住这个顺序:attached顺序是Engine → Ability,detached顺序是Ability → Engine。就像穿衣服和脱衣服------先穿的后脱。
3.4 资源释放的重要性
我见过不少插件在onDetachedFromEngine里什么都不做,这是非常危险的:
typescript
// ❌ 错误示范:什么都不清理
onDetachedFromEngine(binding: FlutterPluginBinding): void {
// 啥也不干
}
// ✅ 正确示范:完整清理
onDetachedFromEngine(binding: FlutterPluginBinding): void {
if (this.channel != null) {
this.channel.setMethodCallHandler(null); // 取消注册
}
this.destroyEngine(); // 销毁引擎
// destroyEngine内部:
// if (this.asrEngine) {
// if (this.isListening) this.asrEngine.cancel(this.sessionId);
// this.asrEngine.shutdown();
// this.asrEngine = null;
// this.isListening = false;
// }
}
不清理资源会导致:
- 内存泄漏:引擎对象不会被GC回收
- 麦克风占用:其他App无法使用麦克风
- 状态混乱:热重载时旧的监听器还在工作
四、MethodChannel 创建与 MethodCallHandler 注册
4.1 MethodChannel 创建详解
typescript
this.channel = new MethodChannel(
binding.getBinaryMessenger(), // 消息传递器
"com.flutter.speech_recognition" // Channel名称
);
| 参数 | 类型 | 说明 |
|---|---|---|
| messenger | BinaryMessenger | 底层消息传递接口 |
| name | string | Channel唯一标识 |
Channel名称的命名规范:
- 使用反向域名格式:
com.company.plugin_name - 必须和Dart端完全一致
- 在整个App中必须唯一
4.2 MethodCallHandler 注册
typescript
this.channel.setMethodCallHandler(this);
这行代码将当前插件实例注册为Channel的方法处理器。之后所有Dart端通过这个Channel发送的方法调用,都会路由到插件的onMethodCall方法。
4.3 onMethodCall 方法分发
typescript
onMethodCall(call: MethodCall, result: MethodResult): void {
switch (call.method) {
case "speech.activate":
this.activate(String(call.args), result).catch((e: Error) => {
console.error(TAG, `activate unhandled error: ${e.message}`);
result.error('SPEECH_ACTIVATION_ERROR', e.message, null);
});
break;
case "speech.listen":
this.startListening(result);
break;
case "speech.cancel":
this.cancel(result);
break;
case "speech.stop":
this.stop(result);
break;
case "speech.destroy":
this.destroyEngine();
result.success(true);
break;
default:
result.notImplemented();
break;
}
}
几个关键点:
- 参数获取 :
call.args获取Dart端传递的参数,call.method获取方法名 - 异步处理 :
activate是async方法,需要用.catch()捕获未处理的异常 - 结果返回 :每个分支都必须调用
result.success()、result.error()或result.notImplemented() - 默认分支 :未知方法返回
result.notImplemented()
🤦 我踩过的坑 :有一次忘记在某个分支里调用
result的方法,导致Dart端的Future永远不会完成,UI就卡住了。每个分支都必须有result响应,这是铁律。
4.4 MethodResult 的三种响应
typescript
// 成功
result.success(true); // 返回成功结果
result.success(null); // 返回成功但无数据
// 错误
result.error( // 返回错误
'ERROR_CODE', // 错误码
'Error message', // 错误信息
null // 错误详情(可选)
);
// 未实现
result.notImplemented(); // 方法未实现
| 方法 | Dart端表现 | 使用场景 |
|---|---|---|
| success(value) | Future正常完成 | 操作成功 |
| error(code, msg, details) | Future抛出PlatformException | 操作失败 |
| notImplemented() | Future抛出MissingPluginException | 方法不存在 |
五、从 Android FlutterPlugin 到 OHOS FlutterPlugin 的映射
5.1 代码对照
把Android和OpenHarmony的实现放在一起对比:
java
// Android实现
public class FlutterSpeechRecognitionPlugin
implements FlutterPlugin, MethodCallHandler, ActivityAware {
private MethodChannel channel;
@Override
public void onAttachedToEngine(FlutterPluginBinding binding) {
channel = new MethodChannel(binding.getBinaryMessenger(),
"com.flutter.speech_recognition");
channel.setMethodCallHandler(this);
}
@Override
public void onDetachedFromEngine(FlutterPluginBinding binding) {
channel.setMethodCallHandler(null);
destroyEngine();
}
@Override
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
switch (call.method) {
case "speech.activate":
activate((String) call.arguments, result);
break;
// ...
}
}
}
typescript
// OpenHarmony实现
export default class FlutterSpeechPlugin
implements FlutterPlugin, MethodCallHandler, AbilityAware {
private channel: MethodChannel | null = null;
getUniqueClassName(): string {
return "FlutterSpeechPlugin";
}
onAttachedToEngine(binding: FlutterPluginBinding): void {
this.channel = new MethodChannel(binding.getBinaryMessenger(),
"com.flutter.speech_recognition");
this.channel.setMethodCallHandler(this);
}
onDetachedFromEngine(binding: FlutterPluginBinding): void {
if (this.channel != null) {
this.channel.setMethodCallHandler(null);
}
this.destroyEngine();
}
onMethodCall(call: MethodCall, result: MethodResult): void {
switch (call.method) {
case "speech.activate":
this.activate(String(call.args), result);
break;
// ...
}
}
}
5.2 语法差异对照表
| 特性 | Java (Android) | ArkTS (OpenHarmony) |
|---|---|---|
| 类声明 | public class X implements Y |
export default class X implements Y |
| 空值类型 | @Nullable |
`Type |
| 类型转换 | (String) call.arguments |
String(call.args) |
| 空值检查 | if (channel != null) |
if (this.channel != null) |
| 方法修饰 | @Override |
无需注解 |
| 访问修饰 | private |
private |
| 异步 | 回调/Future | async/await/Promise |
| 字符串模板 | "text " + var |
text ${var} |
5.3 参数获取方式差异
java
// Android - call.arguments获取参数
String locale = (String) call.arguments;
// 或者
String locale = call.argument("locale");
typescript
// OpenHarmony - call.args获取参数
const locale = String(call.args);
⚠️ 注意 :Android用
call.arguments(复数),OpenHarmony用call.args(缩写)。这个小差异很容易搞混。
5.4 额外的getUniqueClassName方法
OpenHarmony的FlutterPlugin接口多了一个getUniqueClassName方法:
typescript
getUniqueClassName(): string {
return "FlutterSpeechPlugin"
}
这个方法返回插件的唯一标识字符串,Flutter-OHOS引擎用它来防止同一个插件被重复注册。返回值通常就是类名本身。
六、完整的插件类骨架
6.1 最小可运行的插件
如果你要从零开始写一个OpenHarmony Flutter插件,最小的骨架代码是这样的:
typescript
import {
FlutterPlugin,
FlutterPluginBinding,
MethodCall,
MethodCallHandler,
MethodChannel,
MethodResult,
} from '@ohos/flutter_ohos';
const TAG = 'MyPlugin';
export default class MyPlugin implements FlutterPlugin, MethodCallHandler {
private channel: MethodChannel | null = null;
getUniqueClassName(): string {
return "MyPlugin";
}
onAttachedToEngine(binding: FlutterPluginBinding): void {
this.channel = new MethodChannel(binding.getBinaryMessenger(), "com.example.my_plugin");
this.channel.setMethodCallHandler(this);
console.info(TAG, 'Plugin attached to engine');
}
onDetachedFromEngine(binding: FlutterPluginBinding): void {
if (this.channel != null) {
this.channel.setMethodCallHandler(null);
}
console.info(TAG, 'Plugin detached from engine');
}
onMethodCall(call: MethodCall, result: MethodResult): void {
switch (call.method) {
case "getPlatformVersion":
result.success("OpenHarmony");
break;
default:
result.notImplemented();
break;
}
}
}
6.2 flutter_speech的完整骨架
在最小骨架的基础上,flutter_speech还需要:
typescript
import { AbilityAware } from '@ohos/flutter_ohos';
import { AbilityPluginBinding } from '@ohos/flutter_ohos/src/main/ets/embedding/engine/plugins/ability/AbilityPluginBinding';
import { speechRecognizer } from '@kit.CoreSpeechKit';
import { abilityAccessCtrl, common } from '@kit.AbilityKit';
export default class FlutterSpeechPlugin
implements FlutterPlugin, MethodCallHandler, AbilityAware {
private channel: MethodChannel | null = null;
private asrEngine: speechRecognizer.SpeechRecognitionEngine | null = null;
private sessionId: string = '10000';
private isListening: boolean = false;
private lastTranscription: string = '';
private abilityContext: common.UIAbilityContext | null = null;
// FlutterPlugin接口
getUniqueClassName(): string { ... }
onAttachedToEngine(binding: FlutterPluginBinding): void { ... }
onDetachedFromEngine(binding: FlutterPluginBinding): void { ... }
// AbilityAware接口(下一篇详讲)
onAttachedToAbility(binding: AbilityPluginBinding): void { ... }
onDetachedFromAbility(): void { ... }
// MethodCallHandler接口
onMethodCall(call: MethodCall, result: MethodResult): void { ... }
// 业务方法
private async activate(locale: string, result: MethodResult): Promise<void> { ... }
private startListening(result: MethodResult): void { ... }
private cancel(result: MethodResult): void { ... }
private stop(result: MethodResult): void { ... }
private destroyEngine(): void { ... }
// 辅助方法
private convertLocale(locale: string): string { ... }
private isSupportedLocale(locale: string): boolean { ... }
private setupListener(): void { ... }
}
🎯 架构清晰:接口方法负责生命周期管理,业务方法负责具体功能实现,辅助方法负责工具逻辑。三层分明,各司其职。
七、调试FlutterPlugin接口
7.1 日志调试
在每个生命周期方法中添加日志,确认调用时序:
typescript
const TAG = 'FlutterSpeechPlugin';
onAttachedToEngine(binding: FlutterPluginBinding): void {
console.info(TAG, '>>> onAttachedToEngine called');
// ...
console.info(TAG, '<<< onAttachedToEngine done');
}
onDetachedFromEngine(binding: FlutterPluginBinding): void {
console.info(TAG, '>>> onDetachedFromEngine called');
// ...
console.info(TAG, '<<< onDetachedFromEngine done');
}
bash
# 查看日志
hdc hilog | grep "FlutterSpeechPlugin"
7.2 常见问题
| 问题 | 症状 | 原因 | 解决 |
|---|---|---|---|
| onAttachedToEngine未调用 | 插件完全不工作 | 插件未注册 | 检查pubspec.yaml和index.ets |
| Channel名称不匹配 | 方法调用无响应 | Dart和原生端名称不同 | 对比两端的Channel名 |
| result未响应 | Dart端Future卡住 | 忘记调用result方法 | 确保每个分支都有result |
| 重复注册 | 回调触发两次 | getUniqueClassName返回值不唯一 | 确保返回固定字符串 |
7.3 热重载注意事项
Flutter的热重载(Hot Reload)会触发插件的重新绑定:
热重载时:
onDetachedFromEngine() → onAttachedToEngine()
所以onDetachedFromEngine中的清理工作一定要做到位,否则热重载后会出现状态混乱。
八、最佳实践总结
8.1 FlutterPlugin接口实现清单
- 实现
getUniqueClassName(),返回唯一的类名字符串 - 在
onAttachedToEngine中创建MethodChannel并注册Handler - 在
onDetachedFromEngine中取消Handler注册并释放资源 - Channel名称和Dart端完全一致
-
onMethodCall的每个分支都有result响应 - 未知方法返回
result.notImplemented() - 关键位置添加日志便于调试
8.2 代码规范建议
- TAG常量:定义全局TAG用于日志标识
- 空值检查:所有可能为null的变量都要检查
- 异常捕获:业务方法用try-catch包裹
- 资源释放:遵循"谁创建谁释放"原则
总结
本文详细讲解了FlutterPlugin接口在OpenHarmony上的适配实现:
- 接口导入 :从
@ohos/flutter_ohos导入所有必要类型 - FlutterPluginBinding:通过它获取BinaryMessenger创建MethodChannel
- 生命周期:onAttachedToEngine初始化,onDetachedFromEngine清理
- 方法分发:onMethodCall中用switch分发到具体业务方法
- Android映射:接口设计高度一致,主要差异在语法层面
下一篇我们讲AbilityAware接口------如何获取OpenHarmony的UIAbilityContext,这是权限申请和引擎创建的前提。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源: