鸿蒙 Flutter 侧如何处理原生不可用、权限拒绝、结果为空这三类异常
适合谁看
-
正在写 Flutter 页面层异常处理的人
-
刚接入鸿蒙原生能力,担心各种失败路径的人
-
想把降级策略做得更稳的人
问题背景
鸿蒙系统能力的异常往往不是一种,而是至少三种不同性质:
|
异常类型 |
性质 |
示例 |
|---|---|---|
|
原生不可用 |
平台不支持 |
非鸿蒙平台运行时插件不存在 |
|
权限拒绝 |
用户决策 |
用户拒绝鸿蒙麦克风权限 |
|
结果为空 |
正常完成但无内容 |
语音识别完成但没听清 |
这三类情况不应该用同一种处理方式。如果统一弹同一个报错,用户体验会很差。
项目中的真实场景
食界探味当前的通道层已经体现了三类异常的处理:
|
通道 |
异常类型 |
处理方式 |
|---|---|---|
|
|
MissingPluginException |
安全忽略 |
|
|
MissingPluginException |
安全忽略 |
|
|
空返回 |
返回空字符串 |
|
协调器 |
权限拒绝 |
显示错误提示 |
核心实现
一、原生不可用——优先静默降级
当鸿蒙插件不存在时(非鸿蒙平台或插件未注册),会抛 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;
}
// 正常处理...
关键代码位置
|
文件 |
异常处理 |
|---|---|
|
|
空返回兜底 |
|
|
MissingPluginException |
|
|
MissingPluginException |
|
|
权限拒绝 + 空结果提示 |
|
|
鸿蒙权限申请 + 错误回传 |
常见坑
-
三类异常统一弹同一个报错 — 用户不知道发生了什么
-
空字符串被当成异常 — 语音识别没听清是正常场景
-
插件不存在直接导致页面崩溃 — 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 侧应该分别处理:
-
原生不可用 — 静默忽略 + 记日志,不崩溃
-
权限拒绝 — 明确提示用户 + 提供替代方案
-
结果为空 — 当作业务场景处理,给友好提示
这层处理做得细,系统能力体验会稳很多。三类异常不要混为一种"出错",否则用户会频繁看到无意义的报错。
更多推荐





所有评论(0)