前言

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

前20篇我们把flutter_speech的核心功能都讲完了,从第21篇开始进入进阶实践阶段。今天先讲错误处理——这是决定插件"能不能用"和"好不好用"之间的关键差距。

一个没有错误处理的插件,在理想环境下跑得很好,一到真实场景就各种崩溃。网络断了怎么办?权限被拒了怎么办?引擎创建失败怎么办?这些问题都需要优雅地处理。

💡 本文重点:梳理flutter_speech中所有的错误处理路径,分析每种错误的成因和恢复策略。

一、常见错误码分类与含义

1.1 flutter_speech自定义错误码

错误码 触发位置 含义 严重程度
SPEECH_PERMISSION_DENIED activate 麦克风权限被拒绝 ⭐⭐⭐⭐⭐
SPEECH_CONTEXT_ERROR activate UIAbilityContext不可用 ⭐⭐⭐⭐⭐
ERROR_NO_SPEECH_RECOGNITION_AVAILABLE activate 设备不支持语音识别 ⭐⭐⭐⭐⭐
ERROR_LANGUAGE_NOT_SUPPORTED activate 语言不支持 ⭐⭐⭐⭐
SPEECH_ACTIVATION_ERROR activate 引擎创建失败(通用) ⭐⭐⭐⭐
ERROR_ENGINE_NOT_INITIALIZED startListening 引擎未初始化 ⭐⭐⭐
ERROR_SPEECH_LISTEN startListening 启动监听失败 ⭐⭐⭐

1.2 Core Speech Kit引擎错误码

这些错误码通过onError回调传递:

错误码 含义 常见原因
0 无错误 -
1 网络超时 网络不稳定
2 网络异常 无网络连接
3 音频异常 麦克风被占用
4 引擎忙 上一次识别未结束
5 无语音输入 VAD超时,用户没说话
6 识别失败 服务端错误

1.3 错误分类

可恢复性分类:

类别 错误 恢复方式
不可恢复 设备不支持、权限永久拒绝 提示用户,禁用功能
需用户操作 权限首次拒绝、无网络 引导用户操作后重试
自动恢复 网络超时、引擎忙、无语音输入 自动重试

二、引擎未初始化错误处理

2.1 错误场景

用户在没有调用activate的情况下直接调用listen

private startListening(result: MethodResult): void {
  if (!this.asrEngine) {
    result.error('ERROR_ENGINE_NOT_INITIALIZED',
      'Speech engine not initialized. Call activate first.', null);
    return;
  }
  // ...
}

2.2 Dart层的处理

_speech.listen().then((result) {
  setState(() => _isListening = result);
}).catchError((e) {
  if (e is PlatformException && e.code == 'ERROR_ENGINE_NOT_INITIALIZED') {
    // 自动尝试激活
    _speech.activate('zh_CN').then((_) => _speech.listen());
  }
});

2.3 防御性设计

flutter_speech的示例App通过UI状态来防止这种错误——_speechRecognitionAvailablefalse时,Listen按钮不可点击。这是前端防御,比后端报错更好的用户体验。

三、权限拒绝场景的优雅降级

3.1 权限拒绝的两种情况

情况 表现 处理方式
首次拒绝 用户点击"拒绝" 提示并引导重新授权
永久拒绝 用户勾选"不再询问"后拒绝 引导用户去系统设置

3.2 原生端的处理

const grantResult = await atManager.requestPermissionsFromUser(
  this.abilityContext, ['ohos.permission.MICROPHONE']
);
const allGranted = grantResult.authResults.every(
  (status: number) => status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
);
if (!allGranted) {
  result.error('SPEECH_PERMISSION_DENIED', 'Microphone permission denied', null);
  return;
}

3.3 Dart层的降级策略

_speech.activate('zh_CN').then((res) {
  setState(() => _speechRecognitionAvailable = res);
}).catchError((e) {
  if (e is PlatformException) {
    switch (e.code) {
      case 'SPEECH_PERMISSION_DENIED':
        _showPermissionDeniedDialog();
        break;
      case 'ERROR_NO_SPEECH_RECOGNITION_AVAILABLE':
        _showDeviceNotSupportedMessage();
        break;
      default:
        _showGenericErrorMessage(e.message ?? 'Unknown error');
    }
  }
  setState(() => _speechRecognitionAvailable = false);
});

3.4 引导用户去设置页

void _showPermissionDeniedDialog() {
  showDialog(
    context: context,
    builder: (ctx) => AlertDialog(
      title: Text('需要麦克风权限'),
      content: Text('语音识别需要麦克风权限才能工作。请在系统设置中开启。'),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(ctx),
          child: Text('取消'),
        ),
        TextButton(
          onPressed: () {
            Navigator.pop(ctx);
            // OpenHarmony打开应用设置页
            // 需要通过MethodChannel调用原生端的设置跳转
          },
          child: Text('去设置'),
        ),
      ],
    ),
  );
}

四、网络异常对在线识别的影响

4.1 网络异常的表现

flutter_speech使用在线识别模式(online: 1),网络异常会导致:

阶段 网络异常的影响
createEngine 引擎创建失败,抛异常
startListening 可能成功(音频采集不需要网络)
识别过程中 onError回调,错误码1或2

4.2 引擎创建时的网络异常

try {
  this.asrEngine = await speechRecognizer.createEngine({
    language: language,
    online: 1
  });
} catch (e) {
  // 网络异常时,createEngine可能抛出异常
  console.error(TAG, `activate error: ${JSON.stringify(e)}`);
  result.error('SPEECH_ACTIVATION_ERROR',
    `Failed to activate speech recognition: ${JSON.stringify(e)}`, null);
}

4.3 识别过程中的网络异常

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);
}

错误码1(网络超时)和2(网络异常)都会触发onError。

4.4 网络恢复后的重试

void errorHandler() => activateSpeechRecognizer();

flutter_speech示例App的策略是自动重新初始化。网络恢复后,重新activate通常能成功。

🤔 改进建议:可以在重试前先检查网络状态,避免在无网络时反复重试:

void errorHandler() async {
  // 等待一段时间再重试
  await Future.delayed(Duration(seconds: 2));
  activateSpeechRecognizer();
}

五、errorHandler 重新激活策略分析

5.1 当前实现

void errorHandler() => activateSpeechRecognizer();

示例App的错误处理策略极其简单——无条件重新初始化

5.2 执行流程

onError触发
  → Dart: errorHandler()
    → activateSpeechRecognizer()
      → _speech = SpeechRecognition()
      → 设置所有回调
      → activate('zh_CN')
        → 成功 → _speechRecognitionAvailable = true
        → 失败 → _speechRecognitionAvailable = false
                  (不会再次触发errorHandler,因为activate失败不走onError)

5.3 潜在问题:无限重试循环

如果错误在识别过程中持续发生(比如麦克风硬件故障),会出现:

listen → onError → errorHandler → activate → listen → onError → errorHandler → ...

不过实际上这个循环不会真的无限——因为errorHandler只调用activateSpeechRecognizer,不会自动调用listen。用户需要手动点击Listen按钮才会再次开始识别。

5.4 改进方案:带退避的重试

int _retryCount = 0;
static const int _maxRetries = 3;
static const List<int> _retryDelays = [1000, 3000, 10000]; // 毫秒

void errorHandler() async {
  if (_retryCount >= _maxRetries) {
    setState(() => _speechRecognitionAvailable = false);
    _showErrorSnackBar('语音识别暂时不可用,请稍后再试');
    _retryCount = 0;
    return;
  }

  final delay = _retryDelays[_retryCount];
  _retryCount++;

  await Future.delayed(Duration(milliseconds: delay));
  activateSpeechRecognizer();
}

// 在activate成功时重置计数
void onSpeechAvailability(bool result) {
  if (result) _retryCount = 0;
  setState(() => _speechRecognitionAvailable = result);
}

这个方案的优点:

  1. 有限重试:最多重试3次
  2. 退避延迟:每次重试间隔递增(1秒、3秒、10秒)
  3. 用户反馈:超过最大重试次数后提示用户
  4. 自动重置:成功后重置计数器

5.5 各错误的推荐恢复策略

错误 推荐策略 重试次数 用户提示
权限拒绝 不重试,引导设置 0 弹窗引导
设备不支持 不重试 0 永久提示
语言不支持 不重试,切换语言 0 SnackBar
网络超时 自动重试 3次 检查网络
网络异常 等待网络恢复 3次 检查网络
音频异常 自动重试 1次 检查麦克风
引擎忙 延迟重试 2次 稍后再试
无语音输入 不重试(正常行为) 0 提示说话

六、try-catch 在插件中的使用模式

6.1 flutter_speech中的try-catch分布

方法 有try-catch catch中的行为
activate result.error + 日志
startListening result.error + 重置状态 + 日志
cancel result.success(true) + 日志
stop result.success(true) + 日志
destroyEngine 仅日志

6.2 两种catch策略

策略A:报告错误(activate、startListening)

catch (e) {
  result.error('ERROR_CODE', `message: ${JSON.stringify(e)}`, null);
}

策略B:静默成功(cancel、stop)

catch (e) {
  console.error(TAG, `error: ${JSON.stringify(e)}`);
  result.success(true);  // 即使出错也返回成功
}

选择哪种策略取决于操作的语义

  • activate和listen是"请求做某事",失败了应该告诉调用方
  • cancel和stop是"请求停止",即使底层出错,从用户角度来说"停止"已经完成了

6.3 JSON.stringify序列化错误

console.error(TAG, `error: ${JSON.stringify(e)}`);

为什么用JSON.stringify而不是e.message?因为OpenHarmony的异常对象可能不是标准的Error类型,JSON.stringify能输出完整的错误信息:

{"code": 1002003, "message": "Language not supported"}

七、错误处理的完整流程图

Dart调用activate("zh_CN")
    │
    ├── abilityContext == null?
    │   └── 是 → SPEECH_CONTEXT_ERROR → Dart catchError
    │
    ├── 权限申请
    │   └── 拒绝 → SPEECH_PERMISSION_DENIED → Dart catchError
    │
    ├── canIUse检测
    │   └── 不支持 → ERROR_NO_SPEECH_RECOGNITION_AVAILABLE → Dart catchError
    │
    ├── 语言校验
    │   └── 不支持 → ERROR_LANGUAGE_NOT_SUPPORTED → Dart catchError
    │
    ├── createEngine
    │   └── 异常 → SPEECH_ACTIVATION_ERROR → Dart catchError
    │
    └── 成功 → result.success(true) → Dart then

Dart调用listen()
    │
    ├── asrEngine == null?
    │   └── 是 → ERROR_ENGINE_NOT_INITIALIZED → Dart catchError
    │
    ├── startListening
    │   └── 异常 → ERROR_SPEECH_LISTEN → Dart catchError
    │
    └── 成功 → result.success(true) → Dart then

识别过程中
    │
    ├── 正常 → onResult → speech.onSpeech → Dart回调
    │
    └── 异常 → onError → speech.onError → Dart errorHandler
                       → speech.onSpeechAvailability(false)

八、最佳实践总结

8.1 原生端

  1. 每个公开方法都要try-catch:防止未捕获异常导致崩溃
  2. 错误码要有意义:让Dart层能根据错误码做不同处理
  3. 错误信息要包含上下文:包含locale、sessionId等信息便于调试
  4. 销毁方法绝不抛异常:catch后仅记录日志

8.2 Dart端

  1. 区分错误类型:根据错误码做不同的恢复策略
  2. 有限重试:设置最大重试次数,避免无限循环
  3. 用户反馈:每种错误都应该有对应的UI提示
  4. 前端防御:通过UI状态防止无效操作,比后端报错更好

8.3 检查清单

  • 所有MethodCall分支都有result响应
  • 所有异步操作都有try-catch
  • 错误码命名清晰、有文档
  • Dart层对每种错误码有对应处理
  • 重试策略有上限
  • 用户能看到有意义的错误提示

总结

本文梳理了flutter_speech中所有的错误处理路径:

  1. 7个自定义错误码:覆盖权限、能力、语言、引擎、监听等场景
  2. 6个引擎错误码:网络、音频、引擎状态等运行时错误
  3. 两种catch策略:关键操作报告错误,停止操作静默成功
  4. errorHandler自动恢复:简单但有效,建议加重试限制
  5. 权限降级:区分首次拒绝和永久拒绝,引导用户操作

下一篇我们讲调试技巧与日志分析——如何高效地定位和解决flutter_speech中的问题。

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


相关资源:

请添加图片描述

Logo

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

更多推荐