Flutter三方库适配OpenHarmony【flutter_speech】— 语音识别停止与取消

前言

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

语音识别不只是"开始"那么简单,怎么结束 同样重要。flutter_speech提供了两种结束方式------stopcancel,它们的语义完全不同:stop是"我说完了,给我结果",cancel是"算了不要了"。

这两个方法的代码都很短,加起来不到30行。但别小看这30行代码,里面涉及的状态管理边界场景处理是整个插件中最容易出bug的地方。重复调用怎么办?引擎未初始化怎么办?正在识别中cancel了,结果回调还会触发吗?

我在测试阶段遇到了好几个和停止/取消相关的问题,今天把这些经验都整理出来。

💡 本文对应源码FlutterSpeechPlugin.etsstop方法(第237-248行)和cancel方法(第224-235行)。

一、stop(finish)与 cancel 的语义区别

1.1 核心区别

操作 Dart层方法 原生层API 语义 是否返回结果
停止 _speech.stop() asrEngine.finish(sessionId) "我说完了" ✅ 返回最终结果
取消 _speech.cancel() asrEngine.cancel(sessionId) "不要了" ❌ 不返回结果

1.2 行为差异

stop(finish)

复制代码
用户说"今天天气怎么样" → 点击stop
    │
    ├── 引擎停止音频采集
    ├── 处理已采集的音频
    ├── 返回最终识别结果:"今天天气怎么样"
    ├── onResult(isLast=true) 触发
    └── onComplete 触发

cancel

复制代码
用户说"今天天气怎么样" → 点击cancel
    │
    ├── 引擎立即停止
    ├── 丢弃所有未处理的音频
    ├── 不返回结果
    └── onComplete 可能触发(但无结果)

1.3 类比理解

把语音识别比作拍照

  • stop = 按下快门,等照片保存完成
  • cancel = 关闭相机,不拍了

或者比作写邮件

  • stop = 点击"发送"
  • cancel = 点击"丢弃草稿"

1.4 三平台的命名对比

操作 Android iOS OpenHarmony
正常停止 stopListening() endAudio() finish(sessionId)
取消 cancel() (task.cancel()) cancel(sessionId)

📌 命名差异 :Android叫stopListening,OpenHarmony叫finish。虽然名字不同,但语义一致------都是"正常结束并获取结果"。

二、finish 方法:正常结束并获取最终结果

2.1 源码实现

typescript 复制代码
private stop(result: MethodResult): void {
  try {
    if (this.asrEngine && this.isListening) {
      this.asrEngine.finish(this.sessionId);
      this.isListening = false;
    }
    result.success(true);
  } catch (e) {
    console.error(TAG, `stop error: ${JSON.stringify(e)}`);
    result.success(true);
  }
}

2.2 逐行解析

第1行if (this.asrEngine && this.isListening)

  • 双重检查:引擎存在 正在监听
  • 如果引擎不存在或没在监听,直接跳过,返回success

第2行this.asrEngine.finish(this.sessionId)

  • 调用Core Speech Kit的finish方法
  • 传入sessionId指定要停止的会话
  • 引擎会处理剩余音频并触发最终结果回调

第3行this.isListening = false

  • 立即将状态标记为"未监听"
  • 不等回调触发再改状态,避免竞态条件

第4行result.success(true)

  • 无论是否执行了finish,都返回success
  • 这是因为"停止"操作本身不应该失败------即使没在监听,停止也是合理的

2.3 为什么catch中也返回success

typescript 复制代码
catch (e) {
  console.error(TAG, `stop error: ${JSON.stringify(e)}`);
  result.success(true);  // ← 错误了也返回success?
}

这是一个设计决策:stop操作的语义是"请求停止",即使底层出了异常,从用户的角度来说"停止"这个动作已经完成了。返回error会让Dart层的Future抛异常,可能导致UI出现不必要的错误提示。

🤔 我的看法:这种处理方式有争议。有人认为应该把错误传回去让调用方知道。但在实际使用中,stop失败通常是因为引擎已经自己停了(比如VAD超时),这时候返回error反而会让用户困惑。所以flutter_speech选择了"静默成功"的策略。

2.4 finish后的回调时序

调用finish后,引擎会触发以下回调:

复制代码
asrEngine.finish(sessionId)
    │
    ├── 引擎处理剩余音频(可能需要几百毫秒)
    │
    ├── onResult(result, isLast=true)  ← 最终结果
    │   ├── speech.onSpeech(text)
    │   └── speech.onRecognitionComplete(text)
    │
    └── onComplete(sessionId, message)
        └── (isListening已经是false,跳过)

⚠️ 注意finish是异步的------调用后不会立即触发回调。引擎需要时间处理剩余音频。所以result.success(true)是在回调之前返回的,Dart层收到success后还需要等待onRecognitionComplete回调才能拿到最终结果。

三、cancel 方法:立即中断不返回结果

3.1 源码实现

typescript 复制代码
private cancel(result: MethodResult): void {
  try {
    if (this.asrEngine && this.isListening) {
      this.asrEngine.cancel(this.sessionId);
      this.isListening = false;
    }
    result.success(true);
  } catch (e) {
    console.error(TAG, `cancel error: ${JSON.stringify(e)}`);
    result.success(true);
  }
}

3.2 与stop的代码差异

把两个方法放在一起对比:

typescript 复制代码
// stop
this.asrEngine.finish(this.sessionId);   // ← 唯一的区别

// cancel
this.asrEngine.cancel(this.sessionId);   // ← 唯一的区别

代码结构完全一样,唯一的区别就是调用的API不同:finish vs cancel

3.3 cancel后的回调行为

复制代码
asrEngine.cancel(sessionId)
    │
    ├── 引擎立即停止(不处理剩余音频)
    │
    ├── onComplete(sessionId, message)  ← 可能触发
    │   └── (isListening已经是false,跳过)
    │
    └── 不会触发onResult(isLast=true)  ← 关键区别

cancel后不会触发 onResult(isLast=true),所以Dart层不会收到speech.onRecognitionComplete事件。这正是cancel的语义------"不要结果了"。

3.4 cancel的使用场景

场景 说明
用户点击"取消"按钮 用户主动放弃本次识别
切换语言后重新识别 先cancel旧的,再start新的
页面退出 离开语音识别页面时清理
防重入 startListening中先cancel旧会话

四、isListening 状态管理与防重入处理

4.1 isListening的生命周期

typescript 复制代码
private isListening: boolean = false;

isListening在以下位置被修改:

位置 操作 新值 说明
startListening 开始识别 true 标记为监听中
startListening(防重入) cancel旧会话 false → true 先false再true
stop 停止识别 false 标记为未监听
cancel 取消识别 false 标记为未监听
onResult(isLast=true) 收到最终结果 false 识别自然结束
onComplete 会话完成 false 兜底处理
onError 发生错误 false 错误恢复

4.2 状态转换图

复制代码
                    startListening
    false ─────────────────────────────► true
      ▲                                    │
      │                                    │
      │  stop/cancel                       │  onResult(isLast=true)
      │  onError                           │  onComplete
      │  onComplete                        │
      │                                    │
      └────────────────────────────────────┘

4.3 防重入的实现

startListening中:

typescript 复制代码
if (this.isListening) {
  this.asrEngine.cancel(this.sessionId);
  this.isListening = false;
}

这段代码确保了同一时间只有一个活跃的识别会话。如果用户快速连续点击"开始"按钮,不会出现多个会话冲突。

复制代码
用户快速点击两次"开始":

第1次点击:
  isListening = false → startListening → isListening = true

第2次点击(第1次还在识别中):
  isListening = true → cancel第1次 → isListening = false → startListening → isListening = true

4.4 竞态条件分析

有一个潜在的竞态条件:stop方法将isListening设为false,但onResult(isLast=true)回调可能在之后触发,也会将isListening设为false

typescript 复制代码
// stop方法
this.asrEngine.finish(this.sessionId);
this.isListening = false;  // ← 第1次设false

// 稍后,onResult回调触发
onResult(sessionId, result) {
  if (result.isLast) {
    plugin.isListening = false;  // ← 第2次设false(重复但无害)
  }
}

这种重复设置是无害的 ------false设两次还是false。flutter_speech的设计选择了"宁可重复也不遗漏"的策略。

五、边界场景处理:重复调用、引擎未初始化

5.1 边界场景清单

场景 stop的行为 cancel的行为
正常识别中 finish + 返回结果 cancel + 不返回结果
未在识别(isListening=false) 跳过finish,返回success 跳过cancel,返回success
引擎未初始化(asrEngine=null) 跳过,返回success 跳过,返回success
连续调用两次stop 第1次正常,第2次跳过 第1次正常,第2次跳过
stop后立即cancel stop正常,cancel跳过 -
cancel后立即stop cancel正常,stop跳过 -

5.2 引擎未初始化

typescript 复制代码
if (this.asrEngine && this.isListening) {
  // 只有引擎存在且正在监听时才执行
}
result.success(true);  // 无论如何都返回success

如果用户在没有调用activate的情况下直接调用stopcancelasrEngine为null,条件不满足,直接返回success。不会报错,也不会崩溃。

5.3 连续调用

复制代码
stop() → isListening = false
stop() → isListening已经是false → 跳过finish → 返回success

第二次调用时,isListening已经是false,不会重复调用finish。这是安全的。

5.4 stop和cancel交叉调用

复制代码
stop() → finish + isListening = false
cancel() → isListening是false → 跳过cancel → 返回success

先stop后cancel,cancel会被跳过。反过来也一样。这是正确的行为------已经停止了就不需要再取消。

六、stop/cancel 与 Dart 层的交互

6.1 Dart层的调用

dart 复制代码
// 停止识别
Future stop() => _channel.invokeMethod("speech.stop");

// 取消识别
Future cancel() => _channel.invokeMethod("speech.cancel");

6.2 示例App中的使用

dart 复制代码
// 停止按钮
ElevatedButton(
  onPressed: _isListening ? () => _speech.stop() : null,
  child: Text('Stop'),
),

// 取消按钮
ElevatedButton(
  onPressed: _isListening ? () => _speech.cancel() : null,
  child: Text('Cancel'),
),

注意按钮的onPressed只在_isListeningtrue时才可点击。这是UI层的防重入------如果没在识别,按钮是灰色的。

6.3 stop后的结果获取

stop后,Dart层通过recognitionCompleteHandler回调获取最终结果:

dart 复制代码
_speech.setRecognitionCompleteHandler((String text) {
  setState(() {
    _transcription = text;
    _isListening = false;
  });
});

cancel后,recognitionCompleteHandler不会被调用 ,所以_transcription保持之前的值(部分结果或空字符串)。

6.4 完整的交互时序

stop场景

复制代码
Dart: _speech.stop()
  → Native: onMethodCall("speech.stop")
    → Native: asrEngine.finish(sessionId)
    → Native: result.success(true)
  → Dart: stop() Future完成

  (稍后)
  → Native: onResult(text, isLast=true)
    → Native: channel.invokeMethod('speech.onSpeech', text)
    → Native: channel.invokeMethod('speech.onRecognitionComplete', text)
  → Dart: recognitionResultHandler(text)
  → Dart: recognitionCompleteHandler(text)

cancel场景

复制代码
Dart: _speech.cancel()
  → Native: onMethodCall("speech.cancel")
    → Native: asrEngine.cancel(sessionId)
    → Native: result.success(true)
  → Dart: cancel() Future完成

  (不会有后续回调)

七、与Android实现的对比

7.1 代码对比

Android stop

java 复制代码
private void stopListening(MethodChannel.Result result) {
    try {
        if (speechRecognizer != null && isListening) {
            speechRecognizer.stopListening();
            isListening = false;
        }
        result.success(true);
    } catch (Exception e) {
        result.success(true);
    }
}

OpenHarmony stop

typescript 复制代码
private stop(result: MethodResult): void {
  try {
    if (this.asrEngine && this.isListening) {
      this.asrEngine.finish(this.sessionId);
      this.isListening = false;
    }
    result.success(true);
  } catch (e) {
    console.error(TAG, `stop error: ${JSON.stringify(e)}`);
    result.success(true);
  }
}

7.2 差异点

差异 Android OpenHarmony
API名称 stopListening() finish(sessionId)
需要sessionId ❌ 不需要 ✅ 需要
错误日志 有console.error
代码结构 几乎一样 几乎一样

两个平台的实现高度相似,这说明flutter_speech的适配做得很好------保持了跨平台的一致性。

八、最佳实践与注意事项

8.1 何时用stop,何时用cancel

场景 推荐操作 原因
用户说完了,想要结果 stop 需要最终识别结果
用户想重新说 cancel 不需要当前结果
切换语言 cancel 旧语言的结果没用
页面退出 cancel 不需要结果了
超时处理 stop 尽量保留已识别的内容
错误恢复 cancel 清理状态重新开始

8.2 注意事项

  1. 不要在stop后立即startListening:finish是异步的,需要等onComplete回调后再开始新的识别
  2. cancel后可以立即startListening:cancel是立即生效的
  3. 不要忘记更新UI状态:stop/cancel后要更新按钮状态
  4. destroyEngine前先stop/cancel:确保识别已停止再销毁引擎
typescript 复制代码
// ✅ 正确的销毁顺序
if (this.isListening) {
  this.asrEngine.cancel(this.sessionId);  // 先停止
}
this.asrEngine.shutdown();  // 再销毁

总结

本文详细讲解了flutter_speech中语音识别的停止与取消:

  1. 语义区别:stop(finish)返回最终结果,cancel丢弃结果
  2. 实现结构:两个方法代码几乎一样,只是调用的API不同
  3. 状态管理:通过isListening标志防止重复操作和竞态条件
  4. 边界处理:引擎未初始化、未在监听、重复调用都能安全处理
  5. 错误策略:异常时也返回success,避免不必要的错误提示

下一篇我们讲引擎销毁与资源释放 ------destroyEngine方法的实现和资源管理的最佳实践。

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


相关资源:

相关推荐
哈__2 小时前
基础入门 Flutter for OpenHarmony:flutter_slidable 列表滑动操作详解
flutter
哈__2 小时前
基础入门 Flutter for OpenHarmony:mobile_device_identifier 设备唯一标识详解
flutter
松叶似针3 小时前
Flutter三方库适配OpenHarmony【secure_application】— 应用生命周期回调注册
flutter·harmonyos
无巧不成书02183 小时前
【RN鸿蒙教学|第12课时】进阶实战+全流程复盘:痛点攻坚与实战项目初始化
react native·华为·开源·交互·harmonyos
哈__4 小时前
基础入门 Flutter for OpenHarmony:battery_plus 电池状态监控详解
flutter
键盘鼓手苏苏4 小时前
Flutter for OpenHarmony 实战:flutter_redux 全局状态机与单向数据流
flutter·华为·harmonyos
阿林来了5 小时前
Flutter三方库适配OpenHarmony【flutter_speech】— 麦克风权限申请实现
flutter·harmonyos
松叶似针6 小时前
Flutter三方库适配OpenHarmony【secure_application】— 窗口事件监听与应用切换检测
flutter·harmonyos
阿林来了6 小时前
Flutter三方库适配OpenHarmony【flutter_speech】— OpenHarmony 插件工程创建
flutter·harmonyos·鸿蒙