Flutter三方库适配OpenHarmony【flutter_speech】— MethodChannel 双向通信实现

前言

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

前面几篇我们分别讲了权限申请、引擎创建、监听器、启动停止、资源释放。这些功能散落在不同的方法中,是时候把它们串起来了。

MethodChannel是连接Dart层和原生层的桥梁,所有的交互都通过它完成。flutter_speech的通信是双向的------Dart层可以调用原生方法(如activate、listen),原生层也可以主动通知Dart层(如识别结果、错误事件)。

我在适配过程中发现,理解通信的全貌比理解单个方法更重要。很多bug不是某个方法写错了,而是通信时序搞混了------比如在引擎还没创建好的时候就发了onSpeechAvailability,或者在channel已经被注销后还在invokeMethod。

今天我们把flutter_speech中所有的通信路径都梳理一遍,画一张完整的通信地图。

💡 本文重点 :不是讲某个方法的实现细节(前面已经讲过了),而是从通信协议 的角度把所有交互串起来。

一、Dart → Native:onMethodCall 方法分发

1.1 通信方向

复制代码
Dart层                          Native层(OpenHarmony)
──────                          ────────────────────
_channel.invokeMethod(...)  →   onMethodCall(call, result)
                            ←   result.success / result.error

Dart层通过invokeMethod发起调用,Native层通过result返回结果。这是一个请求-响应模式。

1.2 flutter_speech中的所有Dart→Native调用

Dart方法 Channel方法名 参数 返回值 对应Native方法
activate(locale) "speech.activate" String locale bool activate()
listen() "speech.listen" 无 bool startListening()
cancel() "speech.cancel" 无 bool cancel()
stop() "speech.stop" 无 bool stop()
(预留) "speech.destroy" 无 bool destroyEngine()

1.3 Dart层的调用代码

dart 复制代码
class SpeechRecognition {
  static const MethodChannel _channel =
      const MethodChannel('com.flutter.speech_recognition');

  Future activate(String locale) =>
      _channel.invokeMethod("speech.activate", locale);

  Future listen() =>
      _channel.invokeMethod("speech.listen");

  Future cancel() =>
      _channel.invokeMethod("speech.cancel");

  Future stop() =>
      _channel.invokeMethod("speech.stop");
}

1.4 Native层的分发代码

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.5 参数传递方式

方法 Dart传参方式 Native取参方式 参数类型
activate invokeMethod("speech.activate", locale) String(call.args) String
listen invokeMethod("speech.listen") 无参数 -
cancel invokeMethod("speech.cancel") 无参数 -
stop invokeMethod("speech.stop") 无参数 -

flutter_speech的参数传递非常简单------只有activate需要传一个locale字符串,其他方法都不需要参数。

📌 注意 :Dart层用call.arguments(复数),OpenHarmony用call.args(缩写)。这个命名差异在第8篇已经提过,这里再强调一下。

1.6 结果返回方式

每个方法都必须通过result对象返回结果:

typescript 复制代码
// 成功
result.success(true);

// 错误
result.error('ERROR_CODE', 'Error message', null);

// 未实现
result.notImplemented();

Dart层的Future会根据返回方式不同而有不同的表现:

Native返回 Dart Future行为 Dart代码
result.success(value) 正常完成,值为value .then((res) => ...)
result.error(code, msg, details) 抛出PlatformException .catchError((e) => ...)
result.notImplemented() 抛出MissingPluginException .catchError((e) => ...)

二、Native → Dart:channel.invokeMethod 回调通知

2.1 通信方向

复制代码
Native层(OpenHarmony)          Dart层
────────────────────          ──────
channel.invokeMethod(...)  →  _platformCallHandler(call)

Native层通过channel.invokeMethod主动向Dart层发送事件。这是一个推送模式,不需要Dart层先发起请求。

2.2 flutter_speech中的所有Native→Dart事件

Native调用 Channel方法名 参数 触发时机 Dart回调
引擎就绪 "speech.onSpeechAvailability" bool activate成功/失败 availabilityHandler
识别开始 "speech.onRecognitionStarted" null onStart回调 recognitionStartedHandler
识别结果 "speech.onSpeech" String onResult回调 recognitionResultHandler
识别完成 "speech.onRecognitionComplete" String onResult(isLast=true) recognitionCompleteHandler
错误 "speech.onError" int onError回调 errorHandler

2.3 Native层的发送代码

typescript 复制代码
// 1. 引擎可用性通知
this.channel?.invokeMethod('speech.onSpeechAvailability', true);
this.channel?.invokeMethod('speech.onSpeechAvailability', false);

// 2. 识别开始通知
channel?.invokeMethod('speech.onRecognitionStarted', null);

// 3. 实时识别结果
channel?.invokeMethod('speech.onSpeech', result.result);

// 4. 识别完成
channel?.invokeMethod('speech.onRecognitionComplete', result.result);

// 5. 错误通知
channel?.invokeMethod('speech.onError', errorCode);

2.4 Dart层的接收代码

dart 复制代码
SpeechRecognition._internal() {
  _channel.setMethodCallHandler(_platformCallHandler);
}

Future _platformCallHandler(MethodCall call) async {
  switch (call.method) {
    case "speech.onSpeechAvailability":
      availabilityHandler(call.arguments);
      break;
    case "speech.onSpeech":
      recognitionResultHandler(call.arguments);
      break;
    case "speech.onRecognitionStarted":
      recognitionStartedHandler();
      break;
    case "speech.onRecognitionComplete":
      recognitionCompleteHandler(call.arguments);
      break;
    case "speech.onError":
      errorHandler();
      break;
  }
}

2.5 可选链操作符的使用

注意Native层发送事件时使用了?.(可选链操作符):

typescript 复制代码
channel?.invokeMethod('speech.onSpeech', result.result);
//     ↑ 可选链

这是因为channel可能为null(在onDetachedFromEngine之后)。使用?.可以安全地跳过null的情况,不会抛异常。

💡 防御性编程 :在回调中使用channel时,总是用?.而不是.。因为回调可能在引擎销毁后才触发(异步时序问题),此时channel可能已经是null了。

三、speech.onSpeechAvailability 事件传递

3.1 事件含义

speech.onSpeechAvailability告诉Dart层"语音识别是否可用"。

3.2 触发场景

场景 参数值 含义
activate成功 true 引擎就绪,可以开始识别
onError触发 false 引擎出错,不可用

3.3 发送位置

typescript 复制代码
// 位置1:activate成功后
this.channel?.invokeMethod('speech.onSpeechAvailability', true);

// 位置2:onError回调中
channel?.invokeMethod('speech.onSpeechAvailability', false);

3.4 Dart层的处理

dart 复制代码
_speech.setAvailabilityHandler((bool result) {
  setState(() => _speechRecognitionAvailable = result);
});

UI根据_speechRecognitionAvailable的值来控制按钮的可用状态:

dart 复制代码
ElevatedButton(
  onPressed: _speechRecognitionAvailable && !_isListening
      ? () => _speech.listen()
      : null,  // 不可用时按钮灰色
  child: Text('Listen'),
)

3.5 时序分析

复制代码
activate("zh_CN")
    │
    ├── 权限申请 → 成功
    ├── 能力检测 → 支持
    ├── 引擎创建 → 成功
    ├── 设置监听器
    │
    ├── invokeMethod('speech.onSpeechAvailability', true)  ← 通知可用
    │   → Dart: availabilityHandler(true)
    │   → UI: "Listen"按钮变为可点击
    │
    └── result.success(true)
        → Dart: activate() Future完成

📌 注意时序 :onSpeechAvailability(true)是在result.success(true)之前发送的。这意味着Dart层可能先收到可用性通知,再收到activate的返回值。不过在实际使用中,这个顺序差异不影响功能。

四、speech.onSpeech 实时识别结果推送

4.1 事件含义

speech.onSpeech推送实时的识别文本,每次识别引擎产生新的结果都会触发。

4.2 触发频率

一次识别过程中,speech.onSpeech会被触发多次:

复制代码
用户说"打开设置页面":

t=0.3s: speech.onSpeech("打")
t=0.5s: speech.onSpeech("打开")
t=0.8s: speech.onSpeech("打开设置")
t=1.2s: speech.onSpeech("打开设置页面")
t=1.5s: speech.onSpeech("打开设置页面")  ← 最终结果(同时触发onRecognitionComplete)

4.3 参数类型

typescript 复制代码
// Native端发送
channel?.invokeMethod('speech.onSpeech', result.result);
// result.result 是 string 类型
dart 复制代码
// Dart端接收
case "speech.onSpeech":
  recognitionResultHandler(call.arguments);
  // call.arguments 是 String 类型

4.4 UI更新

dart 复制代码
_speech.setRecognitionResultHandler((String text) {
  setState(() {
    _transcription = text;  // 更新显示的文本
  });
});

每次收到新的结果,UI都会更新显示。用户能看到文字一个个蹦出来的效果。

五、speech.onRecognitionComplete 完成事件处理

5.1 事件含义

speech.onRecognitionComplete表示识别已完成,携带最终的识别文本。

5.2 触发位置

这个事件可能从两个地方发出:

typescript 复制代码
// 位置1:onResult回调中(isLast=true时)
onResult(sessionId, result) {
  if (result.isLast) {
    plugin.isListening = false;
    channel?.invokeMethod('speech.onRecognitionComplete', result.result);
  }
}

// 位置2:onComplete回调中(兜底)
onComplete(sessionId, eventMessage) {
  if (plugin.isListening) {
    plugin.isListening = false;
    channel?.invokeMethod('speech.onRecognitionComplete', plugin.lastTranscription);
  }
}

通过isListening标志保证只发送一次。

5.3 与speech.onSpeech的区别

维度 speech.onSpeech speech.onRecognitionComplete
触发次数 多次 1次
含义 实时部分结果 最终完整结果
用途 更新UI显示 标记识别结束
参数 当前文本 最终文本

5.4 Dart层的处理

dart 复制代码
_speech.setRecognitionCompleteHandler((String text) {
  setState(() {
    _transcription = text;
    _isListening = false;  // 标记识别结束
  });
});

收到完成事件后,UI会:

  1. 显示最终文本
  2. 将"正在监听"状态改为"已停止"
  3. 按钮状态恢复

六、完整通信协议图

6.1 一次完整识别的通信时序

复制代码
Dart层                                    Native层
──────                                    ──────

1. activate("zh_CN") ──────────────────►  onMethodCall("speech.activate", "zh_CN")
                                          │ 权限申请...
                                          │ 引擎创建...
                     ◄──────────────────  invokeMethod("speech.onSpeechAvailability", true)
                     ◄──────────────────  result.success(true)

2. listen() ───────────────────────────►  onMethodCall("speech.listen")
                                          │ startListening...
                     ◄──────────────────  result.success(true)
                                          │
                                          │ (引擎开始采集音频)
                     ◄──────────────────  invokeMethod("speech.onRecognitionStarted")
                                          │
                                          │ (用户说话中...)
                     ◄──────────────────  invokeMethod("speech.onSpeech", "你好")
                     ◄──────────────────  invokeMethod("speech.onSpeech", "你好世界")
                                          │
                                          │ (用户停止说话,VAD超时)
                     ◄──────────────────  invokeMethod("speech.onSpeech", "你好世界")
                     ◄──────────────────  invokeMethod("speech.onRecognitionComplete", "你好世界")

3. (识别自然结束,无需手动stop)

6.2 用户手动stop的时序

复制代码
Dart层                                    Native层
──────                                    ──────

(识别进行中...)
                     ◄──────────────────  invokeMethod("speech.onSpeech", "今天天气")

stop() ────────────────────────────────►  onMethodCall("speech.stop")
                                          │ finish(sessionId)
                     ◄──────────────────  result.success(true)
                                          │
                                          │ (引擎处理剩余音频)
                     ◄──────────────────  invokeMethod("speech.onSpeech", "今天天气怎么样")
                     ◄──────────────────  invokeMethod("speech.onRecognitionComplete", "今天天气怎么样")

6.3 错误场景的时序

复制代码
Dart层                                    Native层
──────                                    ──────

activate("en_US") ─────────────────────►  onMethodCall("speech.activate", "en_US")
                                          │ 语言校验失败
                     ◄──────────────────  result.error("ERROR_LANGUAGE_NOT_SUPPORTED", ...)

Dart层                                    Native层
──────                                    ──────

(识别进行中,网络断开)
                                          │ onError(1, "network timeout")
                     ◄──────────────────  invokeMethod("speech.onSpeechAvailability", false)
                     ◄──────────────────  invokeMethod("speech.onError", 1)

七、通信安全性保障

7.1 Channel名称一致性

dart 复制代码
// Dart层
static const MethodChannel _channel =
    const MethodChannel('com.flutter.speech_recognition');
typescript 复制代码
// Native层
this.channel = new MethodChannel(
  binding.getBinaryMessenger(),
  "com.flutter.speech_recognition"  // 必须完全一致
);

如果两端的Channel名称不一致,所有通信都会失效,但不会报错------方法调用会触发MissingPluginException。

7.2 方法名一致性

所有方法名都以speech.为前缀,形成了一个简单的命名空间:

复制代码
Dart → Native:
  speech.activate
  speech.listen
  speech.cancel
  speech.stop
  speech.destroy

Native → Dart:
  speech.onSpeechAvailability
  speech.onRecognitionStarted
  speech.onSpeech
  speech.onRecognitionComplete
  speech.onError

📌 命名规范 :Dart→Native的方法名用动词(activate、listen),Native→Dart的事件名用on前缀(onSpeech、onError)。这种命名方式让通信方向一目了然。

7.3 空值安全

typescript 复制代码
// Native端:channel可能为null
channel?.invokeMethod('speech.onSpeech', result.result);

// Native端:asrEngine可能为null
if (!this.asrEngine) {
  result.error('ERROR_ENGINE_NOT_INITIALIZED', ...);
  return;
}
dart 复制代码
// Dart端:回调可能未设置(late变量)
case "speech.onSpeech":
  recognitionResultHandler(call.arguments);  // 如果未设置会抛异常

⚠️ Dart层的潜在问题 :recognitionResultHandler是late声明的,如果在设置回调之前收到了事件,会抛出LateInitializationError。这是flutter_speech的一个已知限制------必须在调用activate之前设置好所有回调。

7.4 类型安全

事件 Native发送类型 Dart接收类型 是否匹配
onSpeechAvailability boolean dynamic → bool ✅
onRecognitionStarted null 不使用参数 ✅
onSpeech string dynamic → String ✅
onRecognitionComplete string dynamic → String ✅
onError number dynamic → int ✅

Platform Channel会自动处理基本类型的序列化和反序列化,所以类型匹配通常不是问题。

八、通信调试技巧

8.1 日志追踪

在关键通信节点添加日志:

typescript 复制代码
// Native端发送事件时
console.info(TAG, `>>> invokeMethod: speech.onSpeech, args=${result.result}`);
channel?.invokeMethod('speech.onSpeech', result.result);

// Native端收到调用时
onMethodCall(call: MethodCall, result: MethodResult): void {
  console.info(TAG, `<<< onMethodCall: ${call.method}, args=${call.args}`);
  // ...
}
bash 复制代码
# 查看所有通信日志
hdc hilog | grep "FlutterSpeechPlugin" | grep -E "invokeMethod|onMethodCall"

8.2 常见通信问题

问题 症状 原因 解决
Dart调用无响应 Future不完成 某个分支没有调用result 检查所有switch分支
事件收不到 Dart回调不触发 channel为null或Handler未注册 检查生命周期
类型错误 运行时异常 参数类型不匹配 检查发送和接收的类型
重复事件 回调触发多次 监听器注册了多次 确保setupListener只调用一次
事件顺序错乱 UI状态不一致 异步时序问题 用isListening等标志做状态保护

8.3 通信协议检查清单

  • Channel名称两端完全一致
  • 所有Dart→Native方法都有对应的switch分支
  • 所有switch分支都有result响应
  • 所有Native→Dart事件都有对应的_platformCallHandler分支
  • Native端发送事件时使用?.操作符
  • Dart端在使用前设置了所有回调Handler
  • 参数类型两端匹配

总结

本文从通信协议的角度完整梳理了flutter_speech的MethodChannel双向通信:

  1. Dart→Native:5个方法调用(activate、listen、cancel、stop、destroy)
  2. Native→Dart:5个事件通知(onSpeechAvailability、onRecognitionStarted、onSpeech、onRecognitionComplete、onError)
  3. 通信安全:Channel名称一致、方法名一致、空值安全、类型安全
  4. 完整时序:从activate到识别完成的所有通信步骤

下一篇我们讲语言支持与Locale处理------OpenHarmony的语言限制和flutter_speech的应对策略。

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


相关资源:

相关推荐
OH_TPC12 分钟前
HarmonyOS APP开发---"菜谱汇"美食菜谱App,需要用到这个库
harmonyos
老陈说编程44 分钟前
2. 鸿蒙(HarmonyOS)超级终端与生态
华为·harmonyos
2501_919749032 小时前
华为鸿蒙免费时钟工具—小羊时钟
华为·harmonyos
aqi0013 小时前
鸿蒙版本的电子书阅读APP开放源码啦
android·华为·ai编程·harmonyos·鸿蒙
yuanlaile16 小时前
2026年全新Flutter 跨平台开发全栈学习资料整理
flutter·getx·鸿蒙 next·flutter开发鸿蒙·flutter入门实战·flutter全栈
xq952717 小时前
HDC Assistant 插件横空出世
harmonyos
OH_TPC1 天前
【鸿蒙优选三方库】@ohos/aki:一行代码让 ArkTS 调 C++
harmonyos
wenyiyiyiyiyi1 天前
我用鸿蒙 ArkTS 写了个自己天天用的备忘录:待办、课表和长期目标,全塞进一块桌面卡片
harmonyos
恋猫de小郭1 天前
Genkit Dart 1.0 发布,Flutter 原生的 AI Agent 终于完整了
android·前端·flutter
第10086个小白1 天前
flutter桌面端打包方式(Inno Setup Compiler)版本
前端·flutter