Flutter三方库适配OpenHarmony【flutter_speech】— 原始插件源码分析
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net上一篇我们讲了Flutter Plugin的通用机制,今天要把镜头拉近,逐行分析flutter_speech的Dart层源码。整个文件只有85行,但麻雀虽小五脏俱全——单例模式、MethodChannel通信、回调管理、方法映射,该有的都有。我第一次读这个源码的时候,心里想的是"就这?也太
前言
欢迎加入开源鸿蒙跨平台社区: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 文件结构总览
先看一下整个文件的结构:
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 导入分析
import 'dart:async';
import 'package:flutter/services.dart';
只导入了两个包:
- dart:async:提供
Future支持,虽然在新版Dart中不显式导入也能用,但显式声明是好习惯 - flutter/services.dart:提供
MethodChannel、MethodCall、PlatformException等核心类
📌 注意:没有导入
dart:io,说明Dart层本身不做任何平台判断。平台差异完全由原生端处理。这是一个很好的设计——Dart层保持"平台无关"。
1.3 类型定义
typedef void AvailabilityHandler(bool result);
typedef void StringResultHandler(String text);
定义了两个回调类型:
- AvailabilityHandler:接收bool参数,用于通知语音识别是否可用
- StringResultHandler:接收String参数,用于传递识别结果文本
还有一个VoidCallback类型没有在这里定义,因为它是Flutter框架自带的(在package:flutter/foundation.dart中)。
二、MethodChannel 定义与方法调用映射
2.1 Channel声明
static const MethodChannel _channel =
const MethodChannel('com.flutter.speech_recognition');
几个关键点:
- static const:Channel是类级别的常量,所有实例共享同一个Channel
- 命名规范:使用反向域名格式
com.flutter.speech_recognition,这是Flutter插件的通用约定 - 唯一性:这个名称在整个App中必须唯一,如果和其他插件冲突就会出问题
2.2 四个核心方法
/// 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
这种命名方式有两个好处:
- 避免冲突:如果App中有多个插件,前缀可以防止方法名冲突
- 便于调试:在日志中看到
speech.xxx就知道是语音识别相关的调用
三、回调处理机制:Availability、Result、Complete、Error
3.1 五个回调属性
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 回调设置方法
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方法有两个好处:
- 语义更清晰:
setAvailabilityHandler(handler)比speech.availabilityHandler = handler读起来更自然 - 未来扩展性:如果以后需要在设置回调时做一些额外逻辑(比如校验、日志),改setter方法就行,不影响外部调用
四、单例模式设计与 _platformCallHandler 实现
4.1 单例模式实现
static final SpeechRecognition _speech = new SpeechRecognition._internal();
factory SpeechRecognition() => _speech;
SpeechRecognition._internal() {
_channel.setMethodCallHandler(_platformCallHandler);
}
这是Dart中实现单例的经典写法:
- 私有构造函数:
_internal()只能在类内部调用 - 静态实例:
_speech在类加载时就创建了(饿汉式) - 工厂构造函数:
factory SpeechRecognition()每次都返回同一个实例
// 使用方式
final speech1 = SpeechRecognition();
final speech2 = SpeechRecognition();
print(identical(speech1, speech2)); // true,是同一个对象
💡 为什么用单例:语音识别引擎在原生端通常是独占资源的(麦克风同一时间只能被一个进程使用),所以Dart端也应该保持单例,避免多个实例争抢资源。
4.2 _platformCallHandler 详解
这是整个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:回调未初始化
late AvailabilityHandler availabilityHandler;
// 如果在调用activate之前没有setAvailabilityHandler,
// 原生端回调时会抛出LateInitializationError
问题2:错误回调信息丢失
case "speech.onError":
errorHandler(); // errorHandler是VoidCallback,没有传递错误信息
break;
原生端可能传了错误码(call.arguments),但Dart端的errorHandler是VoidCallback类型,无法接收参数。这意味着上层无法知道具体的错误原因。
问题3:无异常保护
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 协议的隐含约定
除了显式的方法名和参数,还有一些隐含约定:
- 调用顺序:必须先
activate再listen,否则原生端引擎未初始化 - 状态互斥:
listen和stop/cancel不能同时调用 - 语言格式:locale参数使用下划线格式(如
zh_CN),原生端可能需要转换 - 结果格式:
onSpeech传递的是完整文本,不是增量
5.3 OpenHarmony适配需要遵守的协议
在做OpenHarmony适配时,原生端必须严格遵守这个协议:
// 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是怎么使用这个插件的:
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);
});
}
初始化步骤:
- 创建
SpeechRecognition实例(单例) - 设置五个回调
- 调用
activate激活引擎 - 根据结果更新UI状态
6.2 语音识别控制
// 开始监听
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的逻辑:
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 可以改进的地方
- 类型安全:返回值应该用泛型
Future<bool>而不是Future - 错误信息:
errorHandler应该传递错误详情 - 空安全:回调属性应该有默认值或null检查
- 日志管理:生产环境不应该有
print语句
如果让我重构,大概会这样改:
// 改进版(仅示意,不修改原代码)
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层源码进行了逐行分析:
- SpeechRecognition类:85行代码,单例模式,结构清晰
- MethodChannel通信:四个方法调用 + 五个事件回调,协议简洁明了
- 回调机制:五种回调覆盖完整的识别生命周期
- 通信协议:明确了Dart层和原生层之间的数据格式和调用约定
- 适配准备:列出了OpenHarmony原生端需要实现的完整清单
理解了Dart层的设计,我们就知道了原生端"应该做什么"。下一篇我们来看Android端是怎么做的,为OpenHarmony适配提供直接参考。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
更多推荐




所有评论(0)