鸿蒙语音识别的 Flutter ↔ ArkTS 完整调用链:权限申请、引擎生命周期与结果回传的时序问题

发布时间:2026/6/26 22:12:31
鸿蒙语音识别的 Flutter ↔ ArkTS 完整调用链:权限申请、引擎生命周期与结果回传的时序问题 适合谁看正在做鸿蒙 Core Speech Kit 接入的 Flutter 开发者遇到语音识别结果收不到或引擎未关闭问题的人想理解 ArkTS 侧异步回调如何安全回传到 Flutter 侧的开发者问题背景语音识别的调用链比普通 MethodChannel 调用复杂得多Flutter 调用startListeningArkTS 侧先申请麦克风权限异步权限通过后创建 ASR 引擎异步设置监听器、启动识别异步系统回调onResult返回识别结果通过MethodResult回传到 Flutter这条链路中有多个异步步骤任何一步的时序问题都可能导致结果丢失或引擎泄漏。项目中的真实场景食界探味在 AI 助手页面支持语音输入。用户点击麦克风按钮后Flutter 调用SpeechRecognitionChannel.startListening()ArkTS 侧SpeechRecognitionPlugin.handleStartListening执行完整流程识别完成后Flutter 收到文本并填入输入框整个流程的时序控制是本篇的重点。核心实现Flutter 侧发起调用// speech_recognition_channel.dart class SpeechRecognitionChannel { static const _channel MethodChannel(com.foodvoyage.speech_recognition); static FutureString startListening() async { try { final result await _channel.invokeMethodString(startListening); return result ?? ; } on MissingPluginException { return ; } catch (e) { AppLogger.warning(Speech recognition failed: $e); return ; } } static Futurevoid stopListening() async { try { await _channel.invokeMethodvoid(stopListening); } on MissingPluginException { // 非鸿蒙平台 } } }Flutter 侧的调用是简单的invokeMethod但 ArkTS 侧的处理要复杂得多。ArkTS 侧handleStartListening 完整流程// SpeechRecognitionPlugin.ets private async handleStartListening(call: MethodCall, result: MethodResult): Promisevoid { // 1. 保存 MethodResult 引用 this.pendingResult result; // 2. 申请麦克风权限 const hasPermission await this.requestMicrophonePermission(); if (!hasPermission) { this.pendingResult null; result.error(PERMISSION_DENIED, 麦克风权限被拒绝, null); return; } // 3. 创建引擎 try { await this.createEngine(); // 4. 设置监听器 this.setupListener(); // 5. 启动识别 this.startListening(); } catch (err) { this.pendingResult null; const error err as BusinessError; result.error(ASR_ERROR, 语音识别启动失败: ${error.message}, null); } }关键设计pendingResult模式。ArkTS 侧不直接在handleStartListening中返回结果而是保存MethodResult引用等异步回调onResult触发时再通过pendingResult.success()回传。权限申请// SpeechRecognitionPlugin.ets private async requestMicrophonePermission(): Promiseboolean { 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) { console.error(TAG, requestPermission failed: ${JSON.stringify(err)}); return false; } }权限申请的关键点使用abilityAccessCtrl.createAtManager()创建权限管理器requestPermissionsFromUser弹出系统权限弹窗返回authResults数组需要检查每个权限的状态如果用户拒绝返回falseFlutter 侧收到PERMISSION_DENIED错误ASR 引擎生命周期// SpeechRecognitionPlugin.ets private createEngine(): Promisevoid { return new Promise((resolve, reject) { const extraParam: Recordstring, Object { locate: CN, recognizerMode: short }; const initParams: speechRecognizer.CreateEngineParams { language: zh-CN, online: 1, extraParams: extraParam }; speechRecognizer.createEngine(initParams, (err, engine) { if (!err) { this.asrEngine engine; resolve(); } else { reject(err); } }); }); }引擎配置参数language: zh-CN中文识别online: 1在线识别需要网络recognizerMode: short短语音模式监听器设置// SpeechRecognitionPlugin.ets private setupListener(): void { if (!this.asrEngine) return; const listener: speechRecognizer.RecognitionListener { onStart: (sessionId, eventMessage) { console.info(TAG, onStart sessionId: ${sessionId}); }, onEvent: (sessionId, eventCode, eventMessage) { console.info(TAG, onEvent code: ${eventCode}); }, onResult: (sessionId, result) { console.info(TAG, onResult: ${JSON.stringify(result)}); if (result.isLast this.pendingResult) { // 识别完成回传结果 this.pendingResult.success(result.result); this.pendingResult null; this.shutdownEngine(); } }, onComplete: (sessionId, eventMessage) { console.info(TAG, onComplete); if (this.pendingResult) { this.pendingResult.success(); this.pendingResult null; } this.shutdownEngine(); }, onError: (sessionId, errorCode, errorMessage) { console.error(TAG, onError code: ${errorCode}); if (this.pendingResult) { this.pendingResult.error(ASR_ERROR, errorMessage, null); this.pendingResult null; } this.shutdownEngine(); } }; this.asrEngine.setListener(listener); }监听器的事件处理事件处理逻辑onStart仅记录日志onEvent仅记录日志onResult当isLast为 true 时回传结果并关闭引擎onComplete回传空结果并关闭引擎onError回传错误并关闭引擎引擎关闭// SpeechRecognitionPlugin.ets private shutdownEngine(): void { try { if (this.asrEngine) { this.asrEngine.shutdown(); this.asrEngine null; console.info(TAG, Engine shutdown); } } catch (err) { console.error(TAG, shutdown error: ${JSON.stringify(err)}); } }引擎关闭的时机onResult收到最终结果后onComplete回调触发时onError错误发生时onDetachedFromEngine插件销毁时停止识别// SpeechRecognitionPlugin.ets private handleStopListening(result: MethodResult): void { try { if (this.asrEngine) { this.asrEngine.finish(this.sessionId); } result.success(null); } catch (err) { const error err as BusinessError; result.error(ASR_ERROR, 停止识别失败: ${error.message}, null); } }finish方法通知引擎停止录音但不会立即关闭引擎。引擎会在onResult或onComplete回调中自然关闭。关键代码位置app/ohos/entry/src/main/ets/plugins/SpeechRecognitionPlugin.ets— 完整的 ArkTS 侧实现app/lib/core/platform/speech_recognition_channel.dart— Flutter 侧调用封装app/ohos/entry/src/main/ets/entryability/EntryAbility.ets— 插件注册鸿蒙侧实现鸿蒙侧的工作分为四个层次权限层abilityAccessCtrl.requestPermissionsFromUser申请麦克风权限引擎层speechRecognizer.createEngine创建 ASR 引擎监听层RecognitionListener处理识别事件回传层pendingResult.success/error将结果回传到 Flutter引擎生命周期状态机创建引擎 → 设置监听器 → 启动识别 → onResult/onComplete/onError → 关闭引擎 ↑ ↓ └──────────────────── 等待下次调用 ←──────────────────────────┘Flutter 侧实现Flutter 侧的职责相对简单调用startListening()发起识别等待invokeMethod返回识别结果调用stopListening()手动停止识别处理MissingPluginException非鸿蒙平台常见坑坑 1pendingResult被覆盖。如果用户快速连续点击麦克风按钮第二次调用会覆盖第一次的pendingResult导致第一次的调用永远收不到结果。需要在handleStartListening开头检查是否已有进行中的识别。坑 2引擎未关闭导致内存泄漏。如果onResult/onComplete/onError都没有触发极端情况引擎会一直占用资源。onDetachedFromEngine中需要强制关闭引擎。坑 3权限拒绝后pendingResult未清理。当前实现中权限拒绝时会设置this.pendingResult null并调用result.error。但如果权限弹窗被用户取消非拒绝行为可能不同。坑 4onComplete和onResult同时触发。如果引擎在返回结果后又触发了onCompletependingResult已经为 null不会重复回传。但如果时序不同可能有问题。坑 5在线识别需要网络。online: 1表示在线识别如果设备无网络引擎创建可能失败。需要考虑离线识别的降级方案。可复用模板// Flutter 侧 - 异步原生调用模板 class AsyncNativeCallT { static const _channel MethodChannel(com.example.async); static FutureT? callWithTimeout( String method, { MapString, dynamic? arguments, Duration timeout const Duration(seconds: 10), }) async { try { final result await _channel.invokeMethodT( method, arguments, ).timeout(timeout); return result; } on TimeoutException { AppLogger.warning(Native call timed out: $method); return null; } on MissingPluginException { return null; } catch (e) { AppLogger.warning(Native call failed: $method, e); return null; } } }// 鸿蒙侧 - pendingResult 模式模板 export default class AsyncPlugin implements FlutterPlugin, MethodCallHandler { private channel: MethodChannel | null null; private pendingResult: MethodResult | null null; private isProcessing false; onMethodCall(call: MethodCall, result: MethodResult): void { if (call.method startAsync) { this.handleStart(call, result); } else if (call.method cancel) { this.handleCancel(result); } } private async handleStart(call: MethodCall, result: MethodResult): Promisevoid { if (this.isProcessing) { result.error(BUSY, Already processing, null); return; } this.isProcessing true; this.pendingResult result; try { await this.doAsyncWork(); } catch (err) { this.pendingResult null; this.isProcessing false; result.error(ERROR, ${err}, null); } } private onAsyncComplete(data: Object): void { if (this.pendingResult) { this.pendingResult.success(data); this.pendingResult null; } this.isProcessing false; } private onAsyncError(error: string): void { if (this.pendingResult) { this.pendingResult.error(ERROR, error, null); this.pendingResult null; } this.isProcessing false; } private handleCancel(result: MethodResult): void { this.pendingResult null; this.isProcessing false; result.success(null); } }本篇总结语音识别的 Flutter ↔ ArkTS 完整调用链核心挑战在于管理多个异步步骤的时序权限申请 → 引擎创建 → 监听器设置 → 识别启动 → 结果回传 → 引擎关闭。pendingResult模式是解决异步回调如何回传到 Flutter的关键设计但需要注意防止覆盖、内存泄漏和时序竞争问题。