前言

欢迎加入开源鸿蒙跨平台社区: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:提供MethodChannelMethodCallPlatformException等核心类

📌 注意:没有导入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');

几个关键点:

  1. static const:Channel是类级别的常量,所有实例共享同一个Channel
  2. 命名规范:使用反向域名格式com.flutter.speech_recognition,这是Flutter插件的通用约定
  3. 唯一性:这个名称在整个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返回的是boollisten也是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 五个回调属性

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()]

🎯 重要发现recognitionResultHandlerrecognitionCompleteHandler都传递完整文本,不是增量。这意味着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方法有两个好处:

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

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

4.1 单例模式实现

static final SpeechRecognition _speech = new SpeechRecognition._internal();

factory SpeechRecognition() => _speech;

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

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

  1. 私有构造函数_internal()只能在类内部调用
  2. 静态实例_speech在类加载时就创建了(饿汉式)
  3. 工厂构造函数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端的errorHandlerVoidCallback类型,无法接收参数。这意味着上层无法知道具体的错误原因

问题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 协议的隐含约定

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

  1. 调用顺序:必须先activatelisten,否则原生端引擎未初始化
  2. 状态互斥listenstop/cancel不能同时调用
  3. 语言格式:locale参数使用下划线格式(如zh_CN),原生端可能需要转换
  4. 结果格式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);
  });
}

初始化步骤:

  1. 创建SpeechRecognition实例(单例)
  2. 设置五个回调
  3. 调用activate激活引擎
  4. 根据结果更新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 可以改进的地方

  1. 类型安全:返回值应该用泛型Future<bool>而不是Future
  2. 错误信息errorHandler应该传递错误详情
  3. 空安全:回调属性应该有默认值或null检查
  4. 日志管理:生产环境不应该有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层源码进行了逐行分析:

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

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

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


相关资源:

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐