前言
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
引擎创建好了,接下来最重要的就是设置监听器 ------告诉引擎"识别到结果了通知我"。这就是setupListener方法要做的事。
监听器是整个语音识别流程中信息密度最高的部分。四个回调方法(onStart、onEvent、onResult、onError)加上一个onComplete,每个回调的触发时机、参数含义、与Dart层的映射关系,都需要搞清楚。
我在实现这个监听器的时候,最头疼的是onResult回调中isLast字段的处理 。Core Speech Kit会多次调用onResult,每次返回一个部分结果,最后一次的isLast为true表示最终结果。这个逻辑和Android的onPartialResults+onResults两个回调的设计不同,需要在一个回调里同时处理部分结果和最终结果。
今天把监听器的每一行代码都掰开讲清楚。
💡 本文对应源码 :
FlutterSpeechPlugin.ets的setupListener方法,第141-178行。

一、setListener 回调接口设计
1.1 监听器接口结构
Core Speech Kit的识别监听器包含五个回调方法:
typescript
interface SpeechRecognitionListener {
onStart(sessionId: string, eventMessage: string): void;
onEvent(sessionId: string, eventCode: number, eventMessage: string): void;
onResult(sessionId: string, result: SpeechRecognitionResult): void;
onComplete(sessionId: string, eventMessage: string): void;
onError(sessionId: string, errorCode: number, errorMessage: string): void;
}
| 回调 | 触发时机 | 触发次数 | 重要程度 |
|---|---|---|---|
| onStart | 识别会话启动 | 1次 | ⭐⭐⭐ |
| onEvent | 识别过程中的事件 | 0-N次 | ⭐⭐ |
| onResult | 收到识别结果 | 1-N次 | ⭐⭐⭐⭐⭐ |
| onComplete | 识别会话完成 | 1次 | ⭐⭐⭐⭐ |
| onError | 发生错误 | 0-1次 | ⭐⭐⭐⭐ |
1.2 与Android RecognitionListener的对比
| Core Speech Kit | Android RecognitionListener | 说明 |
|---|---|---|
| onStart | onReadyForSpeech | 准备就绪 |
| onEvent | onBeginningOfSpeech / onBufferReceived | 过程事件 |
| onResult (isLast=false) | onPartialResults | 部分结果 |
| onResult (isLast=true) | onResults | 最终结果 |
| onComplete | onEndOfSpeech | 识别完成 |
| onError | onError | 错误 |
📌 关键区别 :Android把部分结果和最终结果分成了两个回调(
onPartialResults和onResults),而Core Speech Kit合并成了一个onResult回调,通过isLast字段区分。这种设计更简洁,但处理逻辑需要在一个回调里做分支判断。
1.3 flutter_speech的setupListener完整代码
typescript
private setupListener(): void {
if (!this.asrEngine) return;
const channel = this.channel;
const plugin = this;
this.asrEngine.setListener({
onStart(sessionId: string, eventMessage: string): void {
console.info(TAG, `onStart: sessionId=${sessionId}`);
channel?.invokeMethod('speech.onRecognitionStarted', null);
},
onEvent(sessionId: string, eventCode: number, eventMessage: string): void {
console.info(TAG, `onEvent: code=${eventCode}, message=${eventMessage}`);
},
onResult(sessionId: string, result: speechRecognizer.SpeechRecognitionResult): void {
console.info(TAG, `onResult: ${result.result}, isLast=${result.isLast}`);
plugin.lastTranscription = result.result;
channel?.invokeMethod('speech.onSpeech', result.result);
if (result.isLast) {
plugin.isListening = false;
channel?.invokeMethod('speech.onRecognitionComplete', result.result);
}
},
onComplete(sessionId: string, eventMessage: string): void {
console.info(TAG, `onComplete: ${eventMessage}`);
if (plugin.isListening) {
plugin.isListening = false;
channel?.invokeMethod('speech.onRecognitionComplete', plugin.lastTranscription);
}
},
onError(sessionId: string, errorCode: number, errorMessage: string): void {
console.error(TAG, `onError: code=${errorCode}, message=${errorMessage}`);
plugin.isListening = false;
channel?.invokeMethod('speech.onSpeechAvailability', false);
channel?.invokeMethod('speech.onError', errorCode);
}
});
}
二、onStart 回调:识别会话启动通知
2.1 触发时机
onStart在调用startListening后、引擎开始采集音频时触发。它表示"识别会话已经成功启动,引擎正在等待语音输入"。
startListening(params)
│
└── 引擎初始化音频采集
│
└── onStart(sessionId, eventMessage) ← 在这里触发
│
└── 开始等待用户说话
2.2 实现代码
typescript
onStart(sessionId: string, eventMessage: string): void {
console.info(TAG, `onStart: sessionId=${sessionId}`);
channel?.invokeMethod('speech.onRecognitionStarted', null);
},
做了两件事:
- 打印日志:记录sessionId,便于调试
- 通知Dart层 :调用
speech.onRecognitionStarted事件
2.3 Dart层的响应
dart
// Dart层收到onRecognitionStarted后
Future _platformCallHandler(MethodCall call) async {
switch (call.method) {
case "speech.onRecognitionStarted":
recognitionStartedHandler(); // 调用用户注册的回调
break;
}
}
在示例App中,recognitionStartedHandler用来更新UI状态:
dart
_speech.setRecognitionStartedHandler(() {
setState(() => _isListening = true);
// 可以在这里启动录音动画
});
2.4 参数说明
| 参数 | 类型 | 说明 | flutter_speech是否使用 |
|---|---|---|---|
| sessionId | string | 会话ID | 仅日志记录 |
| eventMessage | string | 事件描述 | 未使用 |
💡 sessionId的作用:如果你的App同时管理多个识别会话(flutter_speech不需要),可以用sessionId区分不同会话的回调。
三、onResult 回调:实时识别结果与 isLast 判断
3.1 这是最核心的回调
onResult是整个监听器中最重要的回调,所有识别结果都通过它传递。
3.2 SpeechRecognitionResult 结构
typescript
interface SpeechRecognitionResult {
result: string; // 识别出的文本
isLast: boolean; // 是否是最终结果
}
| 字段 | 类型 | 说明 |
|---|---|---|
| result | string | 当前识别到的文本内容 |
| isLast | boolean | false=部分结果(还在识别中),true=最终结果(识别结束) |
3.3 onResult的调用时序
一次完整的语音识别,onResult会被调用多次:
用户说:"今天天气怎么样"
onResult("今", isLast=false) ← 第1次:刚开始识别
onResult("今天", isLast=false) ← 第2次:识别更多
onResult("今天天气", isLast=false) ← 第3次:继续
onResult("今天天气怎么样", isLast=false) ← 第4次:接近完成
onResult("今天天气怎么样", isLast=true) ← 第5次:最终结果
每次调用都带有当前的完整识别文本 (不是增量),isLast=true的那次就是最终结果。
3.4 flutter_speech的处理逻辑
typescript
onResult(sessionId: string, result: speechRecognizer.SpeechRecognitionResult): void {
console.info(TAG, `onResult: ${result.result}, isLast=${result.isLast}`);
// 1. 保存最新的识别文本
plugin.lastTranscription = result.result;
// 2. 每次都通知Dart层(实时更新UI)
channel?.invokeMethod('speech.onSpeech', result.result);
// 3. 如果是最终结果,额外发送完成事件
if (result.isLast) {
plugin.isListening = false;
channel?.invokeMethod('speech.onRecognitionComplete', result.result);
}
},
逐行解析:
第1行 :plugin.lastTranscription = result.result
- 保存最新的识别文本到插件成员变量
- 为什么要保存?因为
onComplete回调不带识别结果,需要用这个变量
第2行 :channel?.invokeMethod('speech.onSpeech', result.result)
- 每次收到结果都通知Dart层
- Dart层的
recognitionResultHandler会被调用,更新UI显示实时文本
第3-5行 :if (result.isLast) { ... }
- 只在最终结果时执行
- 将
isListening置为false,表示识别已结束 - 发送
speech.onRecognitionComplete事件,Dart层会调用recognitionCompleteHandler
3.5 为什么每次结果都通知Dart层
你可能会想:只在isLast=true时通知不就行了?为什么部分结果也要通知?
因为实时反馈对用户体验至关重要。用户说话时看到文字一个个蹦出来,会觉得系统在"认真听"。如果等到说完才一次性显示结果,用户会以为系统卡住了。
❌ 只发送最终结果:
用户说话中... (UI无变化,用户以为卡了)
突然显示完整文本
✅ 实时发送部分结果:
"今"
"今天"
"今天天气"
"今天天气怎么样" ← 用户能看到实时变化
3.6 lastTranscription 的作用
lastTranscription是一个"保险"变量。正常情况下,onResult(isLast=true)会带有最终文本。但在某些边界情况下,onComplete可能在onResult(isLast=true)之前触发,这时就需要用lastTranscription来获取最后一次的识别结果。
typescript
// onComplete中的使用
onComplete(sessionId: string, eventMessage: string): void {
if (plugin.isListening) {
plugin.isListening = false;
// 用lastTranscription作为最终结果
channel?.invokeMethod('speech.onRecognitionComplete', plugin.lastTranscription);
}
},
四、onComplete 回调:识别完成处理
4.1 触发时机
onComplete在整个识别会话结束时触发,表示引擎已经停止音频采集和处理。
startListening → onStart → onResult(多次) → onComplete
↑ 在这里触发
4.2 实现代码
typescript
onComplete(sessionId: string, eventMessage: string): void {
console.info(TAG, `onComplete: ${eventMessage}`);
if (plugin.isListening) {
plugin.isListening = false;
channel?.invokeMethod('speech.onRecognitionComplete', plugin.lastTranscription);
}
},
4.3 为什么要检查isListening
typescript
if (plugin.isListening) {
// ...
}
这个检查是为了防止重复发送完成事件 。正常流程中,onResult(isLast=true)已经发送了speech.onRecognitionComplete并将isListening置为false。当onComplete触发时,isListening已经是false了,就不会重复发送。
但如果onComplete先于onResult(isLast=true)触发(异常情况),isListening还是true,这时就需要在onComplete中发送完成事件。
正常流程:
onResult(isLast=true) → isListening=false → onComplete → isListening已经是false,跳过
异常流程:
onComplete → isListening还是true → 发送complete事件 → isListening=false
🎯 这是一种防御性编程 :两个地方都可能发送完成事件,但通过
isListening标志保证只发送一次。
4.4 onComplete vs onResult(isLast=true) 的区别
| 维度 | onResult(isLast=true) | onComplete |
|---|---|---|
| 含义 | 最终识别结果已产生 | 识别会话已结束 |
| 带结果 | ✅ 有result.result | ❌ 只有eventMessage |
| 触发保证 | 不一定触发(如cancel) | 通常都会触发 |
| 先后顺序 | 通常先触发 | 通常后触发 |
五、onError 回调:错误码解析与异常恢复
5.1 实现代码
typescript
onError(sessionId: string, errorCode: number, errorMessage: string): void {
console.error(TAG, `onError: code=${errorCode}, message=${errorMessage}`);
plugin.isListening = false;
channel?.invokeMethod('speech.onSpeechAvailability', false);
channel?.invokeMethod('speech.onError', errorCode);
}
做了四件事:
- 打印错误日志:记录错误码和错误信息
- 重置监听状态 :
isListening = false - 通知不可用 :发送
speech.onSpeechAvailability(false),告诉Dart层引擎不可用了 - 发送错误事件 :发送
speech.onError(errorCode),Dart层可以做相应处理
5.2 错误码分类
| 错误码 | 含义 | 常见原因 | 恢复策略 |
|---|---|---|---|
| 0 | 无错误 | - | - |
| 1 | 网络超时 | 网络不稳定 | 提示检查网络,重试 |
| 2 | 网络异常 | 无网络连接 | 提示连接网络 |
| 3 | 音频异常 | 麦克风被占用 | 释放麦克风,重试 |
| 4 | 引擎忙 | 上一次识别未结束 | 等待后重试 |
| 5 | 无语音输入 | VAD超时 | 提示用户说话 |
| 6 | 识别失败 | 服务端错误 | 重试 |
5.3 为什么发送onSpeechAvailability(false)
typescript
channel?.invokeMethod('speech.onSpeechAvailability', false);
这行代码告诉Dart层"语音识别当前不可用"。Dart层收到后会更新UI状态,比如禁用"开始识别"按钮。
但这里有个设计上的权衡 :发生错误不一定意味着引擎完全不可用。有些错误(如网络超时)是临时的,重试可能就好了。发送false会导致用户需要重新调用activate才能恢复。
🤔 我的思考 :如果要做得更精细,可以根据错误码判断是否需要发送
false。比如网络超时只发送onError,不发送onSpeechAvailability(false),让用户可以直接重试listen。但flutter_speech的当前实现选择了更保守的策略------出错就标记不可用,让用户重新activate。
5.4 Dart层的错误处理
dart
_speech.setErrorHandler(() {
setState(() {
_isListening = false;
});
// 可以在这里尝试重新激活
_speech.activate('zh_CN').then((res) {
setState(() => _speechRecognitionAvailable = res);
});
});
示例App中,错误处理的策略是自动重新激活。这样用户不需要手动操作就能恢复。
六、onEvent 回调:过程事件
6.1 实现代码
typescript
onEvent(sessionId: string, eventCode: number, eventMessage: string): void {
console.info(TAG, `onEvent: code=${eventCode}, message=${eventMessage}`);
},
flutter_speech对onEvent只做了日志记录,没有向Dart层发送任何事件。
6.2 为什么不处理onEvent
onEvent传递的是识别过程中的中间事件,比如"检测到语音开始"、"检测到语音结束"等。这些事件对于基本的语音识别功能来说不是必需的。
flutter_speech的Dart层API也没有定义对应的回调接口,所以原生端不需要转发这些事件。
6.3 可能的扩展
如果将来要增加更丰富的功能(比如语音活动指示器),可以利用onEvent:
typescript
// 未来可能的扩展
onEvent(sessionId: string, eventCode: number, eventMessage: string): void {
console.info(TAG, `onEvent: code=${eventCode}, message=${eventMessage}`);
// 例如:检测到语音开始
if (eventCode === 1) {
channel?.invokeMethod('speech.onVoiceStart', null);
}
// 例如:检测到语音结束
if (eventCode === 2) {
channel?.invokeMethod('speech.onVoiceEnd', null);
}
},
七、this 指向问题与闭包技巧
7.1 问题背景
在setupListener中有一个容易被忽略的细节------this指向问题:
typescript
private setupListener(): void {
if (!this.asrEngine) return;
const channel = this.channel; // ← 为什么要这样做?
const plugin = this; // ← 为什么要这样做?
this.asrEngine.setListener({
onResult(sessionId, result) {
plugin.lastTranscription = result.result; // 用plugin而不是this
channel?.invokeMethod('speech.onSpeech', result.result); // 用channel而不是this.channel
}
});
}
7.2 原因解释
在JavaScript/TypeScript中,对象字面量中的方法的this指向调用者 ,而不是定义时的外部对象。如果在回调中直接用this,它指向的是监听器对象本身,而不是FlutterSpeechPlugin实例。
typescript
// ❌ 错误:this指向监听器对象,不是插件实例
this.asrEngine.setListener({
onResult(sessionId, result) {
this.lastTranscription = result.result; // this不是FlutterSpeechPlugin!
this.channel?.invokeMethod(...); // this.channel是undefined!
}
});
// ✅ 正确:用闭包捕获外部变量
const channel = this.channel;
const plugin = this;
this.asrEngine.setListener({
onResult(sessionId, result) {
plugin.lastTranscription = result.result; // plugin是FlutterSpeechPlugin实例
channel?.invokeMethod(...); // channel是MethodChannel实例
}
});
7.3 另一种解决方案:箭头函数
用箭头函数也可以解决this指向问题,因为箭头函数不创建自己的this:
typescript
// 也可以用箭头函数(flutter_speech没有采用这种方式)
this.asrEngine.setListener({
onResult: (sessionId, result) => {
this.lastTranscription = result.result; // 箭头函数的this指向外部
this.channel?.invokeMethod(...);
}
});
flutter_speech选择了闭包变量 的方式而不是箭头函数。两种方式都可以,但闭包变量的方式更明确------一眼就能看出plugin和channel是什么。
💡 ArkTS的this行为:ArkTS基于TypeScript,this的行为和TypeScript一致。如果你不确定this指向什么,用闭包变量是最安全的选择。
八、回调与Dart层事件的映射关系
8.1 完整映射表
| Core Speech Kit回调 | 触发条件 | Dart层事件 | Dart回调 |
|---|---|---|---|
| onStart | 识别启动 | speech.onRecognitionStarted | recognitionStartedHandler() |
| onEvent | 过程事件 | (不转发) | - |
| onResult (isLast=false) | 部分结果 | speech.onSpeech | recognitionResultHandler(text) |
| onResult (isLast=true) | 最终结果 | speech.onSpeech + speech.onRecognitionComplete | recognitionResultHandler + recognitionCompleteHandler |
| onComplete | 会话结束 | speech.onRecognitionComplete (仅isListening时) | recognitionCompleteHandler(text) |
| onError | 错误 | speech.onSpeechAvailability(false) + speech.onError | availabilityHandler(false) + errorHandler() |
8.2 一次完整识别的事件流
用户说"你好世界":
Native端 Dart端
──────── ──────
onStart("10000", "")
→ invokeMethod('speech.onRecognitionStarted') → recognitionStartedHandler()
onResult("10000", {result:"你好", isLast:false})
→ invokeMethod('speech.onSpeech', "你好") → recognitionResultHandler("你好")
onResult("10000", {result:"你好世界", isLast:false})
→ invokeMethod('speech.onSpeech', "你好世界") → recognitionResultHandler("你好世界")
onResult("10000", {result:"你好世界", isLast:true})
→ invokeMethod('speech.onSpeech', "你好世界") → recognitionResultHandler("你好世界")
→ invokeMethod('speech.onRecognitionComplete', "你好世界")
→ recognitionCompleteHandler("你好世界")
onComplete("10000", "")
→ (isListening已经是false,跳过)
8.3 错误场景的事件流
网络超时场景:
Native端 Dart端
──────── ──────
onStart("10000", "")
→ invokeMethod('speech.onRecognitionStarted') → recognitionStartedHandler()
onError("10000", 1, "network timeout")
→ invokeMethod('speech.onSpeechAvailability', false) → availabilityHandler(false)
→ invokeMethod('speech.onError', 1) → errorHandler()
九、调试监听器
9.1 日志输出
每个回调都有日志输出,方便调试:
bash
# 查看所有回调日志
hdc hilog | grep "FlutterSpeechPlugin" | grep -E "onStart|onEvent|onResult|onComplete|onError"
正常识别的日志输出:
FlutterSpeechPlugin: onStart: sessionId=10000
FlutterSpeechPlugin: onResult: 你好, isLast=false
FlutterSpeechPlugin: onResult: 你好世界, isLast=false
FlutterSpeechPlugin: onResult: 你好世界, isLast=true
FlutterSpeechPlugin: onComplete:
9.2 常见问题
| 问题 | 症状 | 原因 | 解决 |
|---|---|---|---|
| 回调不触发 | 无日志输出 | setupListener未调用或引擎未创建 | 检查activate流程 |
| onResult只触发一次 | 只有最终结果 | VAD参数太敏感 | 调整vadEnd参数 |
| onError频繁触发 | 错误码5 | 无语音输入 | 检查麦克风或增大vadBegin |
| 结果为空字符串 | result.result为"" | 说话声音太小 | 靠近麦克风 |
| Dart层收不到事件 | 原生日志正常但Dart无响应 | channel为null | 检查onAttachedToEngine |
总结
本文详细讲解了flutter_speech语音识别监听器的完整实现:
- 五个回调:onStart、onEvent、onResult、onComplete、onError各司其职
- onResult是核心 :通过
isLast字段区分部分结果和最终结果 - 双重完成保障:onResult(isLast=true)和onComplete都可能发送完成事件,通过isListening标志防止重复
- 错误处理:onError中重置状态并通知Dart层不可用
- this指向:用闭包变量解决回调中的this指向问题
- 事件映射:每个原生回调都有对应的Dart层事件
下一篇我们讲语音识别的启动与参数配置 ------startListening方法中StartParams的详细解析。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源: