Flutter三方库适配OpenHarmony【flutter_speech】— 原始插件源码分析

前言

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

上一篇我们讲了Flutter Plugin的通用机制,今天要把镜头拉近,逐行分析flutter_speech的Dart层源码 。整个flutter_speech.dart文件只有85行,但麻雀虽小五脏俱全------单例模式、MethodChannel通信、回调管理、方法映射,该有的都有。

我第一次读这个源码的时候,心里想的是"就这?也太简单了吧"。但当我开始做OpenHarmony适配,需要搞清楚每个方法调用的时序和数据格式时,才发现之前的"简单"只是因为没有深入思考。

今天我会把这85行代码拆开来讲,每一个设计决策背后的考量都会分析到。这些理解对后续的原生端实现至关重要。

💡 阅读建议 :建议打开lib/flutter_speech.dart源码对照阅读,效果更好。

一、flutter_speech.dart 核心类 SpeechRecognition 解析

1.1 文件结构总览

先看一下整个文件的结构:

dart 复制代码
import 'dart:async';
import 'package:flutter/services.dart';

typedef void AvailabilityHandler(bool result);
typedef void StringResultHandler(String text);

class SpeechRecognition {
  // MethodChannel定义
  // 单例实现
  // 回调属性
  // 公开方法:activate, listen, cancel, stop
  // 私有方法:_platformCallHandler
  // 回调设置方法
}
代码区域 行数 职责
import语句 1-3 导入依赖
类型定义 5-6 回调函数类型
Channel定义 10-11 MethodChannel声明
单例实现 13-18 工厂构造函数
回调属性 21-29 五个回调handler
核心方法 32-43 activate/listen/cancel/stop
回调处理 44-65 _platformCallHandler
setter方法 67-83 回调设置器

总共85行,没有一行多余的代码。这种简洁的风格我很喜欢。

1.2 导入分析

dart 复制代码
import 'dart:async';
import 'package:flutter/services.dart';

只导入了两个包:

  • dart:async :提供Future支持,虽然在新版Dart中不显式导入也能用,但显式声明是好习惯
  • flutter/services.dart :提供MethodChannel、MethodCall、PlatformException等核心类

📌 注意 :没有导入dart:io,说明Dart层本身不做任何平台判断。平台差异完全由原生端处理。这是一个很好的设计------Dart层保持"平台无关"。

1.3 类型定义

dart 复制代码
typedef void AvailabilityHandler(bool result);
typedef void StringResultHandler(String text);

定义了两个回调类型:

  • AvailabilityHandler:接收bool参数,用于通知语音识别是否可用
  • StringResultHandler:接收String参数,用于传递识别结果文本

还有一个VoidCallback类型没有在这里定义,因为它是Flutter框架自带的(在package:flutter/foundation.dart中)。

二、MethodChannel 定义与方法调用映射

2.1 Channel声明

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

几个关键点:

  1. static const:Channel是类级别的常量,所有实例共享同一个Channel
  2. 命名规范 :使用反向域名格式com.flutter.speech_recognition,这是Flutter插件的通用约定
  3. 唯一性:这个名称在整个App中必须唯一,如果和其他插件冲突就会出问题

2.2 四个核心方法

dart 复制代码
/// ask for speech recognizer permission
Future activate(String locale) =>
    _channel.invokeMethod("speech.activate", locale);

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

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

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

方法映射表:

Dart方法 Channel方法名 参数类型 参数说明 返回值
activate speech.activate String 语言代码如"zh_CN" Future (bool)
listen speech.listen 无 - Future (bool)
cancel speech.cancel 无 - Future (bool)
stop speech.stop 无 - Future (bool)

🤔 我的思考 :这四个方法的返回值类型都是Future(没有泛型参数),这意味着返回值类型是dynamic。在实际使用中,activate返回的是bool,listen也是bool。如果是我来设计,会用Future<bool>来增加类型安全性。不过这不影响功能。

2.3 方法命名的设计考量

所有Channel方法名都以speech.为前缀:

复制代码
speech.activate
speech.listen
speech.cancel
speech.stop
speech.onSpeechAvailability
speech.onSpeech
speech.onRecognitionStarted
speech.onRecognitionComplete
speech.onError

这种命名方式有两个好处:

  1. 避免冲突:如果App中有多个插件,前缀可以防止方法名冲突
  2. 便于调试 :在日志中看到speech.xxx就知道是语音识别相关的调用

三、回调处理机制:Availability、Result、Complete、Error

3.1 五个回调属性

dart 复制代码
late AvailabilityHandler availabilityHandler;
late StringResultHandler recognitionResultHandler;
late VoidCallback recognitionStartedHandler;
late StringResultHandler recognitionCompleteHandler;
late VoidCallback errorHandler;

使用late关键字声明,意味着这些属性在使用前必须被赋值,否则会抛出LateInitializationError。

回调 类型 触发时机 参数
availabilityHandler (bool) → void 引擎可用性变化 true=可用, false=不可用
recognitionResultHandler (String) → void 实时识别结果 当前识别文本
recognitionStartedHandler () → void 识别会话开始 无
recognitionCompleteHandler (String) → void 识别完成 最终识别文本
errorHandler () → void 发生错误 无

3.2 回调的生命周期

一次完整的语音识别流程中,回调的触发顺序:

复制代码
用户调用 activate("zh_CN")
    │
    ├── [原生端初始化引擎]
    │
    └── availabilityHandler(true)  ← 引擎就绪
         │
用户调用 listen()
    │
    ├── recognitionStartedHandler()  ← 开始监听
    │
    ├── recognitionResultHandler("你")      ← 部分结果
    ├── recognitionResultHandler("你好")    ← 部分结果更新
    ├── recognitionResultHandler("你好世界") ← 部分结果更新
    │
    ├── [用户调用stop()或引擎自动停止]
    │
    └── recognitionCompleteHandler("你好世界") ← 最终结果

如果中间出错:

复制代码
    ├── errorHandler()  ← 错误发生
    │
    └── [示例App中errorHandler会重新调用activateSpeechRecognizer()]

🎯 重要发现 :recognitionResultHandler和recognitionCompleteHandler都传递完整文本,不是增量。这意味着Dart端不需要做文本拼接,直接替换显示即可。这个设计简化了上层的状态管理。

3.3 回调设置方法

dart 复制代码
void setAvailabilityHandler(AvailabilityHandler handler) =>
    availabilityHandler = handler;

void setRecognitionResultHandler(StringResultHandler handler) =>
    recognitionResultHandler = handler;

void setRecognitionStartedHandler(VoidCallback handler) =>
    recognitionStartedHandler = handler;

void setRecognitionCompleteHandler(StringResultHandler handler) =>
    recognitionCompleteHandler = handler;

void setErrorHandler(VoidCallback handler) => errorHandler = handler;

这些setter方法非常简单,就是直接赋值。有人可能会问为什么不直接暴露属性让外部赋值?我觉得用setter方法有两个好处:

  1. 语义更清晰 :setAvailabilityHandler(handler)比speech.availabilityHandler = handler读起来更自然
  2. 未来扩展性:如果以后需要在设置回调时做一些额外逻辑(比如校验、日志),改setter方法就行,不影响外部调用

四、单例模式设计与 _platformCallHandler 实现

4.1 单例模式实现

dart 复制代码
static final SpeechRecognition _speech = new SpeechRecognition._internal();

factory SpeechRecognition() => _speech;

SpeechRecognition._internal() {
  _channel.setMethodCallHandler(_platformCallHandler);
}

这是Dart中实现单例的经典写法:

  1. 私有构造函数 :_internal()只能在类内部调用
  2. 静态实例 :_speech在类加载时就创建了(饿汉式)
  3. 工厂构造函数 :factory SpeechRecognition()每次都返回同一个实例
dart 复制代码
// 使用方式
final speech1 = SpeechRecognition();
final speech2 = SpeechRecognition();
print(identical(speech1, speech2)); // true,是同一个对象

💡 为什么用单例:语音识别引擎在原生端通常是独占资源的(麦克风同一时间只能被一个进程使用),所以Dart端也应该保持单例,避免多个实例争抢资源。

4.2 _platformCallHandler 详解

这是整个Dart层最核心的方法,负责处理所有来自原生端的回调:

dart 复制代码
Future _platformCallHandler(MethodCall call) async {
  print("_platformCallHandler call ${call.method} ${call.arguments}");
  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;
    default:
      print('Unknowm method ${call.method} ');
  }
}

逐行分析:

行 代码 说明
1 Future _platformCallHandler(MethodCall call) async 异步方法,接收MethodCall
2 print(...) 调试日志,生产环境建议去掉
3-4 case "speech.onSpeechAvailability" 引擎可用性变化,参数是bool
6-7 case "speech.onSpeech" 实时识别结果,参数是String
9-10 case "speech.onRecognitionStarted" 识别开始,无参数
12-13 case "speech.onRecognitionComplete" 识别完成,参数是String
15-16 case "speech.onError" 错误发生,无参数
18 default: print(...) 未知方法,打印警告

4.3 潜在问题分析

读源码的时候我发现了几个潜在问题:

问题1:回调未初始化

dart 复制代码
late AvailabilityHandler availabilityHandler;
// 如果在调用activate之前没有setAvailabilityHandler,
// 原生端回调时会抛出LateInitializationError

问题2:错误回调信息丢失

dart 复制代码
case "speech.onError":
  errorHandler();  // errorHandler是VoidCallback,没有传递错误信息
  break;

原生端可能传了错误码(call.arguments),但Dart端的errorHandler是VoidCallback类型,无法接收参数。这意味着上层无法知道具体的错误原因。

问题3:无异常保护

dart 复制代码
case "speech.onSpeech":
  recognitionResultHandler(call.arguments);  // 如果arguments不是String会怎样?
  break;

没有对call.arguments做类型检查。虽然正常情况下原生端传的都是正确类型,但防御性编程还是有必要的。

🤦 实话实说 :这些问题在大多数使用场景下不会暴露出来,因为开发者通常会在调用activate之前就设置好所有回调。但如果你要做一个生产级的插件,这些细节都需要处理。

五、Dart 层与原生层的通信协议约定

5.1 协议总览

通过源码分析,我们可以总结出Dart层和原生层之间的通信协议:

Dart → 原生(方法调用):

复制代码
┌─────────────────┬──────────────┬──────────────┐
│ 方法名           │ 参数          │ 期望返回值    │
├─────────────────┼──────────────┼──────────────┤
│ speech.activate  │ String locale │ bool         │
│ speech.listen    │ null          │ bool         │
│ speech.cancel    │ null          │ bool         │
│ speech.stop      │ null          │ bool         │
└─────────────────┴──────────────┴──────────────┘

原生 → Dart(事件回调):

复制代码
┌──────────────────────────────┬──────────────┐
│ 方法名                        │ 参数          │
├──────────────────────────────┼──────────────┤
│ speech.onSpeechAvailability   │ bool         │
│ speech.onSpeech               │ String       │
│ speech.onRecognitionStarted   │ null         │
│ speech.onRecognitionComplete  │ String       │
│ speech.onError                │ int/null     │
└──────────────────────────────┴──────────────┘

5.2 协议的隐含约定

除了显式的方法名和参数,还有一些隐含约定:

  1. 调用顺序 :必须先activate再listen,否则原生端引擎未初始化
  2. 状态互斥 :listen和stop/cancel不能同时调用
  3. 语言格式 :locale参数使用下划线格式(如zh_CN),原生端可能需要转换
  4. 结果格式 :onSpeech传递的是完整文本,不是增量

5.3 OpenHarmony适配需要遵守的协议

在做OpenHarmony适配时,原生端必须严格遵守这个协议:

typescript 复制代码
// OpenHarmony端必须实现的方法处理
onMethodCall(call: MethodCall, result: MethodResult): void {
  switch (call.method) {
    case "speech.activate":   // 必须处理
    case "speech.listen":     // 必须处理
    case "speech.cancel":     // 必须处理
    case "speech.stop":       // 必须处理
    default:
      result.notImplemented(); // 未知方法返回notImplemented
  }
}

// OpenHarmony端必须发送的回调
channel.invokeMethod('speech.onSpeechAvailability', bool);
channel.invokeMethod('speech.onSpeech', string);
channel.invokeMethod('speech.onRecognitionStarted', null);
channel.invokeMethod('speech.onRecognitionComplete', string);
channel.invokeMethod('speech.onError', errorCode);

🎯 适配的核心原则:Dart层代码一行不改,所有适配工作都在原生端完成。原生端只要遵守协议,Dart层就能正常工作。

六、示例应用中的使用方式分析

6.1 初始化流程

看看example/lib/main.dart是怎么使用这个插件的:

dart 复制代码
void activateSpeechRecognizer() {
  _speech = SpeechRecognition();
  _speech.setAvailabilityHandler(onSpeechAvailability);
  _speech.setRecognitionStartedHandler(onRecognitionStarted);
  _speech.setRecognitionResultHandler(onRecognitionResult);
  _speech.setRecognitionCompleteHandler(onRecognitionComplete);
  _speech.setErrorHandler(errorHandler);
  _speech.activate('zh_CN').then((res) {
    setState(() => _speechRecognitionAvailable = res);
  }).catchError((e) {
    setState(() => _speechRecognitionAvailable = false);
  });
}

初始化步骤:

  1. 创建SpeechRecognition实例(单例)
  2. 设置五个回调
  3. 调用activate激活引擎
  4. 根据结果更新UI状态

6.2 语音识别控制

dart 复制代码
// 开始监听
void start() => _speech.activate(selectedLang.code).then((_) {
  return _speech.listen().then((result) {
    setState(() => _isListening = result);
  });
});

// 取消识别
void cancel() =>
    _speech.cancel().then((_) => setState(() => _isListening = false));

// 停止识别
void stop() => _speech.stop().then((_) {
  setState(() => _isListening = false);
});

🤔 一个细节 :start方法先调用activate再调用listen。这意味着每次开始识别都会重新激活引擎。在Android端这可能没问题(SpeechRecognizer创建很快),但在OpenHarmony端,createEngine是异步操作且有一定开销,频繁创建可能影响性能。

6.3 OpenHarmony平台特殊处理

示例代码中有一段专门针对OpenHarmony的逻辑:

dart 复制代码
void _selectLangHandler(Language lang) {
  if (Platform.isOhos && !lang.code.startsWith('zh')) {
    _messengerKey.currentState?.showSnackBar(
      const SnackBar(content: Text('当前设备仅支持中文语音识别')),
    );
    return;
  }
  setState(() => selectedLang = lang);
}

这段代码在Dart层做了语言校验:如果是OpenHarmony平台且选择了非中文语言,直接弹出提示而不是让请求到达原生端。这是一种"快速失败"的策略,用户体验更好。

七、源码质量评估与改进建议

7.1 优点

方面 评价 说明
代码简洁 ⭐⭐⭐⭐⭐ 85行代码,没有冗余
API设计 ⭐⭐⭐⭐ 方法命名清晰,使用简单
单例模式 ⭐⭐⭐⭐⭐ 实现标准,线程安全
回调分离 ⭐⭐⭐⭐ 不同事件独立处理

7.2 可以改进的地方

  1. 类型安全 :返回值应该用泛型Future<bool>而不是Future
  2. 错误信息 :errorHandler应该传递错误详情
  3. 空安全:回调属性应该有默认值或null检查
  4. 日志管理 :生产环境不应该有print语句

如果让我重构,大概会这样改:

dart 复制代码
// 改进版(仅示意,不修改原代码)
class SpeechRecognition {
  // 使用可空类型代替late
  AvailabilityHandler? _availabilityHandler;
  StringResultHandler? _recognitionResultHandler;

  // 返回值增加泛型
  Future<bool> activate(String locale) async {
    final result = await _channel.invokeMethod<bool>("speech.activate", locale);
    return result ?? false;
  }

  // 回调增加空检查
  Future _platformCallHandler(MethodCall call) async {
    switch (call.method) {
      case "speech.onSpeechAvailability":
        _availabilityHandler?.call(call.arguments as bool);
        break;
      // ...
    }
  }
}

💡 但是:对于一个开源插件来说,现有的实现已经足够好了。过度设计反而会增加复杂度。在做OpenHarmony适配时,我们遵循原有设计就好。

八、为OpenHarmony适配做准备

8.1 适配清单

通过源码分析,我们可以列出OpenHarmony原生端需要实现的完整清单:

复制代码
必须实现的方法处理:
├── speech.activate(locale: String) → bool
├── speech.listen() → bool
├── speech.cancel() → bool
└── speech.stop() → bool

必须发送的回调:
├── speech.onSpeechAvailability(available: bool)
├── speech.onSpeech(text: String)
├── speech.onRecognitionStarted()
├── speech.onRecognitionComplete(text: String)
└── speech.onError(errorCode: int)

8.2 需要注意的技术点

技术点 说明 优先级
locale转换 zh_CN → zh-CN 高
权限申请 MICROPHONE权限 高
引擎生命周期 创建/销毁时机 高
状态管理 isListening标志 中
错误处理 异常捕获和错误码 中
资源释放 引擎销毁 中

8.3 下一步计划

接下来的两篇文章会分析Android和iOS的原生实现,看看它们是怎么实现这些功能的。这些参考对OpenHarmony适配非常有价值------虽然API不同,但思路是相通的。

总结

本文对flutter_speech的Dart层源码进行了逐行分析:

  1. SpeechRecognition类:85行代码,单例模式,结构清晰
  2. MethodChannel通信:四个方法调用 + 五个事件回调,协议简洁明了
  3. 回调机制:五种回调覆盖完整的识别生命周期
  4. 通信协议:明确了Dart层和原生层之间的数据格式和调用约定
  5. 适配准备:列出了OpenHarmony原生端需要实现的完整清单

理解了Dart层的设计,我们就知道了原生端"应该做什么"。下一篇我们来看Android端是怎么做的,为OpenHarmony适配提供直接参考。

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


相关资源:

相关推荐
梦想不只是梦与想2 小时前
HarmonyOS应用分层架构设计
harmonyos·分层架构·一次开发,多端部署
MardaWang4 小时前
当滚动逃离了框架 ——HarmonyOS Web 与原生混排滚动的冲突本质与解法
harmonyos·arkts·鸿蒙·deveco studio
tsqtsqtsq03095 小时前
DevEco Studio 介绍
harmonyos
传奇开心果编程8 小时前
【现代声明式UI学与练】第4课 列表渲染与 key——如何高效渲染列表、key 的作用、列表重排时的状态保持
学习·flutter·react native·ui·swiftui·android jetpack
HwJack208 小时前
【共创稿事节】HarmonyOS 7文旅展陈展厅大空间 3DGS 重建的分块策略与拼接踩坑
3d·华为·harmonyos
熊猫钓鱼>_>10 小时前
Kotlin Multiplatform for OpenHarmony 实战:为 kotlin-inject 实现依赖注入适配
开发语言·华为·kotlin·ai编程·inject·鸿蒙·openharmony
m0_7381858211 小时前
Flutter 鸿蒙化实战:media_info 适配 OpenHarmony,媒体信息与缩略图
flutter·华为·harmonyos·鸿蒙·媒体
m0_7381858212 小时前
Flutter 鸿蒙化实战:just_audio 适配 OpenHarmony,功能强大的播放器
flutter·华为·harmonyos·鸿蒙
翼辉cto12 小时前
Kotlin Multiplatform 三方库 SQLDelight 的 OpenHarmony 鸿蒙化适配实战
开发语言·kotlin·harmonyos