适合谁看

  • 正在写 Flutter 页面层异常处理的人

  • 刚接入鸿蒙原生能力,担心各种失败路径的人

  • 想把降级策略做得更稳的人

问题背景

鸿蒙系统能力的异常往往不是一种,而是至少三种不同性质:

异常类型

性质

示例

原生不可用

平台不支持

非鸿蒙平台运行时插件不存在

权限拒绝

用户决策

用户拒绝鸿蒙麦克风权限

结果为空

正常完成但无内容

语音识别完成但没听清

这三类情况不应该用同一种处理方式。如果统一弹同一个报错,用户体验会很差。

项目中的真实场景

食界探味当前的通道层已经体现了三类异常的处理:

通道

异常类型

处理方式

anti_peep_protection_channel.dart

MissingPluginException

安全忽略

intent_navigation_channel.dart

MissingPluginException

安全忽略

speech_recognition_channel.dart

空返回

返回空字符串

协调器

权限拒绝

显示错误提示

核心实现

一、原生不可用——优先静默降级

当鸿蒙插件不存在时(非鸿蒙平台或插件未注册),会抛 MissingPluginException

鸿蒙侧的处理(不需要额外处理,插件不存在时 MethodChannel 自动抛异常)

Flutter 侧的处理:

// anti_peep_protection_channel.dart

static Future<void> _invoke(String method) async {
  try {
    await _channel.invokeMethod<void>(method);
  } on MissingPluginException {
    // 鸿蒙插件不可用 — 安全忽略,不崩溃
  } catch (e, stackTrace) {
    AppLogger.warning(
      'Anti-peep protection channel call failed: $method',
      e,
      stackTrace,
    );
  }
}
// intent_navigation_channel.dart

static Future<void> _consumePending() async {
  try {
    final payload = await _channel.invokeMethod<Object?>(
      'consumePendingNavigation',
    );
    final navigation = _parseArguments(payload);
    if (navigation != null) {
      _navigate(navigation);
    }
  } on MissingPluginException {
    // 非鸿蒙平台 — 忽略
  } catch (e) {
    AppLogger.warning('consumePendingNavigation failed: $e');
  }
}

处理原则:

  • 安全忽略,不崩溃

  • 记录日志,方便排查

  • 不影响页面其他功能

为什么不能崩溃:

非鸿蒙平台运行时:
  MissingPluginException 抛出
  → 如果没有 catch → 页面崩溃
  → 如果有 catch → 安全忽略,页面正常运行

二、权限拒绝——应该明确反馈给用户

当鸿蒙麦克风权限被拒绝时,原生侧会返回错误。这不是"可忽略"的情况,因为它是用户决策导致的失败。

鸿蒙侧的处理:

// SpeechRecognitionPlugin.ets

private async handleStartListening(call: MethodCall, result: MethodResult): Promise<void> {
  this.pendingResult = result;

  const hasPermission = await this.requestMicrophonePermission();
  if (!hasPermission) {
    this.pendingResult = null;
    result.error('PERMISSION_DENIED', '麦克风权限被拒绝', null);
    return;
  }

  // 继续识别...
}

private async requestMicrophonePermission(): Promise<boolean> {
  try {
    const atManager = abilityAccessCtrl.createAtManager();
    const permissions: Permissions[] = ['ohos.permission.MICROPHONE'];
    const context = getContext(this);
    const grantResult = await atManager.requestPermissionsFromUser(context, permissions);
    return grantResult.authResults.every(
      status => status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
    );
  } catch (err) {
    return false;
  }
}

Flutter 侧的处理:

// ai_explore_coordinator.dart

Future<void> startVoiceInput() async {
  if (!mounted) return;
  state = state.copyWith(
    status: AiSessionStatus.listening,
    errorMessage: null,
  );

  try {
    final text = await SpeechRecognitionChannel.startListening();
    if (!mounted) return;
    if (text.isEmpty) {
      state = state.copyWith(
        status: AiSessionStatus.error,
        errorMessage: '未听清,请再说一次',
      );
      return;
    }
    await submitQuery(text);
  } catch (e) {
    AppLogger.error('[AI助手] 语音识别出错: $e');
    if (!mounted) return;
    state = state.copyWith(
      status: AiSessionStatus.error,
      errorMessage: '语音识别出错,请手动输入',
    );
  }
}

处理原则:

  • 给用户明确的错误提示

  • 提供替代方案(如"请手动输入")

  • 不继续走后续流程

权限拒绝 vs 其他错误的区别:

场景

用户感知

处理方式

权限拒绝

"我拒绝了,但不知道后果"

提示用户,提供替代方案

引擎失败

"系统出问题了"

提示用户,建议重试

空结果

"没听清"

提示用户再说一次

三、结果为空——要区分"空结果"和"异常"

语音识别完成后可能返回空字符串。这时候更合理的处理是当作一次无内容输入,而不是当作系统错误。

鸿蒙侧的处理:

// SpeechRecognitionPlugin.ets

onResult: (sessionId: string, result: speechRecognizer.SpeechRecognitionResult) => {
  if (result.isLast && this.pendingResult) {
    this.pendingResult.success(result.result);  // 可能是空字符串
    this.pendingResult = null;
    this.shutdownEngine();
  }
}

onComplete: (sessionId: string, eventMessage: string) => {
  if (this.pendingResult) {
    this.pendingResult.success('');  // 没有结果时返回空字符串
    this.pendingResult = null;
  }
  this.shutdownEngine();
}

Flutter 侧的处理:

// speech_recognition_channel.dart

static Future<String> startListening({String language = 'zh-CN'}) async {
  final result = await _channel.invokeMethod<String>(
    'startListening',
    {'language': language},
  );
  return result ?? '';  // null 时返回空字符串
}
// 协调器的空值处理

final text = await SpeechRecognitionChannel.startListening();
if (text.isEmpty) {
  state = state.copyWith(
    status: AiSessionStatus.error,
    errorMessage: '未听清,请再说一次',
  );
  return;
}

TTS 的空值处理:

// ai_explore_coordinator.dart

Future<void> speakText(String text) async {
  final cleaned = _stripForTts(text);
  if (cleaned.isEmpty) return;  // 清理后为空则不播报
  // 继续播报...
}

处理原则:

  • 当作正常业务场景处理,不是系统错误

  • 给用户友好的提示("未听清"而不是"系统错误")

  • 不弹技术性错误信息

空结果 vs 异常的区别:

场景

性质

处理方式

语音识别返回空

正常完成,没听清

提示"未听清,请再说一次"

TTS 文本清理后为空

正常完成,内容不适合播报

静默跳过

搜索结果为空

正常完成,没有匹配

显示"没有找到匹配结果"

原生返回 null

可能是异常

兜底为空字符串

四、三类异常的完整对比

维度

原生不可用

权限拒绝

结果为空

性质

平台不支持

用户决策

正常完成

错误码

MissingPluginException

PERMISSION_DENIED

无(正常返回)

用户感知

无(功能不可用)

需要知道后果

需要知道结果

处理方式

静默忽略

明确提示

友好提示

是否崩溃

必须 catch

必须 catch

不需要 catch

日志级别

warning

error

info

五、三类异常的处理模板

模板 1:原生不可用

static Future<T> safeInvoke<T>(String method, Map<String, dynamic>? args) async {
  try {
    final result = await _channel.invokeMethod<T>(method, args);
    return result;
  } on MissingPluginException {
    AppLogger.info('Plugin not available on this platform: $method');
    return null as T;
  } catch (e) {
    AppLogger.warning('Channel call failed: $method', e);
    return null as T;
  }
}

模板 2:权限拒绝

Future<void> startVoiceInput() async {
  try {
    final text = await SpeechRecognitionChannel.startListening();
    if (text.isEmpty) {
      // 空结果 → 友好提示
      state = state.copyWith(
        status: AiSessionStatus.error,
        errorMessage: '未听清,请再说一次',
      );
      return;
    }
    await submitQuery(text);
  } on PlatformException catch (e) {
    if (e.code == 'PERMISSION_DENIED') {
      // 权限拒绝 → 明确提示 + 替代方案
      state = state.copyWith(
        status: AiSessionStatus.error,
        errorMessage: '请在设置中开启麦克风权限,或手动输入',
      );
    } else {
      // 其他错误 → 通用提示
      state = state.copyWith(
        status: AiSessionStatus.error,
        errorMessage: '语音识别出错,请手动输入',
      );
    }
  }
}

模板 3:结果为空

final text = await SomeChannel.someMethod();
if (text == null || text.isEmpty) {
  // 空结果 → 当作业务场景处理
  showFriendlyMessage('没有找到相关内容');
  return;
}
// 正常处理...

关键代码位置

文件

异常处理

app/lib/core/platform/speech_recognition_channel.dart

空返回兜底

app/lib/core/platform/anti_peep_protection_channel.dart

MissingPluginException

app/lib/core/platform/intent_navigation_channel.dart

MissingPluginException

app/lib/core/ai/ai_explore_coordinator.dart

权限拒绝 + 空结果提示

app/ohos/entry/src/main/ets/plugins/SpeechRecognitionPlugin.ets

鸿蒙权限申请 + 错误回传

常见坑

  • 三类异常统一弹同一个报错 — 用户不知道发生了什么

  • 空字符串被当成异常 — 语音识别没听清是正常场景

  • 插件不存在直接导致页面崩溃 — MissingPluginException 必须 catch

  • 权限拒绝后还继续走后续流程 — 应该立即终止并提示

  • 没有区分"权限拒绝"和"引擎失败" — 两者的用户感知不同

  • TTS 文本清理后为空还调用播报 — 应该静默跳过

  • 空结果时弹技术性错误信息 — 应该显示友好提示

可复用模板

三类异常处理决策树

收到异常或空结果
  │
  ├─ MissingPluginException?
  │   └─ 是 → 静默忽略 + 记日志
  │
  ├─ PlatformException (PERMISSION_DENIED)?
  │   └─ 是 → 提示用户 + 提供替代方案
  │
  ├─ 结果为 null 或空字符串?
  │   └─ 是 → 当作业务场景处理("未听清"/"没有匹配")
  │
  └─ 其他异常
      └─ 通用错误提示 + 记日志

鸿蒙通道层异常处理模板

static Future<String> safeStartListening() async {
  try {
    final result = await _channel.invokeMethod<String>(
      'startListening',
      {'language': 'zh-CN'},
    );
    return result ?? '';
  } on MissingPluginException {
    return '';  // 非鸿蒙平台
  } on PlatformException catch (e) {
    if (e.code == 'PERMISSION_DENIED') {
      throw PermissionDeniedException('麦克风权限被拒绝');
    }
    throw e;
  }
}

协调器异常处理模板

Future<void> startVoiceInput() async {
  try {
    final text = await SpeechRecognitionChannel.startListening();
    if (text.isEmpty) {
      state = state.copyWith(
        status: AiSessionStatus.error,
        errorMessage: '未听清,请再说一次',
      );
      return;
    }
    await submitQuery(text);
  } catch (e) {
    state = state.copyWith(
      status: AiSessionStatus.error,
      errorMessage: '语音识别出错,请手动输入',
    );
  }
}

本篇总结

鸿蒙原生不可用、权限拒绝、结果为空,是三类完全不同的失败。Flutter 侧应该分别处理:

  1. 原生不可用 — 静默忽略 + 记日志,不崩溃

  2. 权限拒绝 — 明确提示用户 + 提供替代方案

  3. 结果为空 — 当作业务场景处理,给友好提示

这层处理做得细,系统能力体验会稳很多。三类异常不要混为一种"出错",否则用户会频繁看到无意义的报错。

Logo

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

更多推荐