Flutter三方库适配OpenHarmony【flutter_speech】— FlutterPlugin 接口适配

前言

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

上一篇我们搭好了OpenHarmony插件的工程骨架,今天开始往里面填肉------实现FlutterPlugin接口。这是整个适配工作中最核心的部分之一,搞定了这个接口,插件就能被Flutter引擎正确加载和管理了。

我在实现这个接口的时候,最大的感受是:Flutter-OHOS团队做了一件非常聪明的事------他们把OpenHarmony的FlutterPlugin接口设计得和Android端几乎一模一样。如果你之前写过Android的Flutter插件,转到OpenHarmony基本上是"换个语法重写一遍"的事。

当然,"几乎一模一样"不等于"完全一样"。ArkTS和Java毕竟是不同的语言,有些地方的写法差异还是挺大的。今天我就把这些差异一一讲清楚。

💡 本文重点 :FlutterPlugin接口的三个核心方法------getUniqueClassNameonAttachedToEngineonDetachedFromEngine

一、@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);  // 注册方法处理器
}

这段代码做了三件事:

  1. binding中获取BinaryMessenger
  2. 用它创建MethodChannel,指定Channel名称
  3. 将当前插件实例注册为方法处理器

🎯 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;
  // }
}

不清理资源会导致:

  1. 内存泄漏:引擎对象不会被GC回收
  2. 麦克风占用:其他App无法使用麦克风
  3. 状态混乱:热重载时旧的监听器还在工作

四、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;
  }
}

几个关键点:

  1. 参数获取call.args获取Dart端传递的参数,call.method获取方法名
  2. 异步处理activate是async方法,需要用.catch()捕获未处理的异常
  3. 结果返回 :每个分支都必须调用result.success()result.error()result.notImplemented()
  4. 默认分支 :未知方法返回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 代码规范建议

  1. TAG常量:定义全局TAG用于日志标识
  2. 空值检查:所有可能为null的变量都要检查
  3. 异常捕获:业务方法用try-catch包裹
  4. 资源释放:遵循"谁创建谁释放"原则

总结

本文详细讲解了FlutterPlugin接口在OpenHarmony上的适配实现:

  1. 接口导入 :从@ohos/flutter_ohos导入所有必要类型
  2. FlutterPluginBinding:通过它获取BinaryMessenger创建MethodChannel
  3. 生命周期:onAttachedToEngine初始化,onDetachedFromEngine清理
  4. 方法分发:onMethodCall中用switch分发到具体业务方法
  5. Android映射:接口设计高度一致,主要差异在语法层面

下一篇我们讲AbilityAware接口------如何获取OpenHarmony的UIAbilityContext,这是权限申请和引擎创建的前提。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

相关推荐
盐焗西兰花2 小时前
鸿蒙学习实战之路-STG系列(1/11)-屏幕时间守护服务全攻略
学习·华为·harmonyos
空白诗2 小时前
基础入门 Flutter for OpenHarmony:IndexedStack 索引堆叠组件详解
flutter
阿林来了2 小时前
Flutter三方库适配OpenHarmony【flutter_speech】— Core Speech Kit 概述
flutter·harmonyos
一只大侠的侠2 小时前
HarmonyOS实战:React Native实现Popover弹出位置精准控制
react native·华为·harmonyos
松叶似针2 小时前
Flutter三方库适配OpenHarmony【secure_application】— Window 管理与 getLastWindow API
flutter·harmonyos
●VON2 小时前
HarmonyOS应用开发实战(基础篇)Day05-《常见布局Row和Column》
学习·华为·harmonyos·鸿蒙·von
前端不太难3 小时前
鸿蒙 App 架构重建后,为何再次失控
架构·状态模式·harmonyos
空白诗3 小时前
基础入门 Flutter for OpenHarmony:Transform 变换组件详解
flutter
空白诗3 小时前
基础入门 Flutter for OpenHarmony:DecoratedBox 装饰盒子组件详解
flutter