当前位置: 首页 > news >正文

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

适合谁看

  • 正在做鸿蒙 Core Speech Kit 接入的 Flutter 开发者

  • 遇到"语音识别结果收不到"或"引擎未关闭"问题的人

  • 想理解 ArkTS 侧异步回调如何安全回传到 Flutter 侧的开发者

问题背景

语音识别的调用链比普通 MethodChannel 调用复杂得多:

  1. Flutter 调用startListening

  2. ArkTS 侧先申请麦克风权限(异步)

  3. 权限通过后创建 ASR 引擎(异步)

  4. 设置监听器、启动识别(异步)

  5. 系统回调onResult返回识别结果

  6. 通过MethodResult回传到 Flutter

这条链路中有多个异步步骤,任何一步的时序问题都可能导致结果丢失或引擎泄漏。

项目中的真实场景

食界探味在 AI 助手页面支持语音输入。用户点击麦克风按钮后:

  1. Flutter 调用SpeechRecognitionChannel.startListening()

  2. ArkTS 侧SpeechRecognitionPlugin.handleStartListening执行完整流程

  3. 识别完成后,Flutter 收到文本并填入输入框

整个流程的时序控制是本篇的重点。

核心实现

Flutter 侧发起调用

// speech_recognition_channel.dart class SpeechRecognitionChannel { static const _channel = MethodChannel('com.foodvoyage.speech_recognition'); static Future<String> startListening() async { try { final result = await _channel.invokeMethod<String>('startListening'); return result ?? ''; } on MissingPluginException { return ''; } catch (e) { AppLogger.warning('Speech recognition failed: $e'); return ''; } } static Future<void> stopListening() async { try { await _channel.invokeMethod<void>('stopListening'); } on MissingPluginException { // 非鸿蒙平台 } } }

Flutter 侧的调用是简单的invokeMethod,但 ArkTS 侧的处理要复杂得多。

ArkTS 侧:handleStartListening 完整流程

// SpeechRecognitionPlugin.ets private async handleStartListening(call: MethodCall, result: MethodResult): Promise<void> { // 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(): 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) { console.error(TAG, `requestPermission failed: ${JSON.stringify(err)}`); return false; } }

权限申请的关键点:

  • 使用abilityAccessCtrl.createAtManager()创建权限管理器

  • requestPermissionsFromUser弹出系统权限弹窗

  • 返回authResults数组,需要检查每个权限的状态

  • 如果用户拒绝,返回false,Flutter 侧收到PERMISSION_DENIED错误

ASR 引擎生命周期

// SpeechRecognitionPlugin.ets private createEngine(): Promise<void> { return new Promise((resolve, reject) => { const extraParam: Record<string, 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方法通知引擎停止录音,但不会立即关闭引擎。引擎会在onResultonComplete回调中自然关闭。

关键代码位置

  • 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— 插件注册

鸿蒙侧实现

鸿蒙侧的工作分为四个层次:

  1. 权限层abilityAccessCtrl.requestPermissionsFromUser申请麦克风权限

  2. 引擎层speechRecognizer.createEngine创建 ASR 引擎

  3. 监听层RecognitionListener处理识别事件

  4. 回传层pendingResult.success/error将结果回传到 Flutter

引擎生命周期状态机:

创建引擎 → 设置监听器 → 启动识别 → onResult/onComplete/onError → 关闭引擎 ↑ ↓ └──────────────────── 等待下次调用 ←──────────────────────────┘

Flutter 侧实现

Flutter 侧的职责相对简单:

  1. 调用startListening()发起识别

  2. 等待invokeMethod返回识别结果

  3. 调用stopListening()手动停止识别

  4. 处理MissingPluginException(非鸿蒙平台)

常见坑

  • 坑 1:pendingResult被覆盖。如果用户快速连续点击麦克风按钮,第二次调用会覆盖第一次的pendingResult,导致第一次的调用永远收不到结果。需要在handleStartListening开头检查是否已有进行中的识别。

  • 坑 2:引擎未关闭导致内存泄漏。如果onResult/onComplete/onError都没有触发(极端情况),引擎会一直占用资源。onDetachedFromEngine中需要强制关闭引擎。

  • 坑 3:权限拒绝后pendingResult未清理。当前实现中,权限拒绝时会设置this.pendingResult = null并调用result.error。但如果权限弹窗被用户取消(非拒绝),行为可能不同。

  • 坑 4:onCompleteonResult同时触发。如果引擎在返回结果后又触发了onCompletependingResult已经为 null,不会重复回传。但如果时序不同,可能有问题。

  • 坑 5:在线识别需要网络online: 1表示在线识别,如果设备无网络,引擎创建可能失败。需要考虑离线识别的降级方案。

可复用模板

// Flutter 侧 - 异步原生调用模板 class AsyncNativeCall<T> { static const _channel = MethodChannel('com.example.async'); static Future<T?> callWithTimeout( String method, { Map<String, dynamic>? arguments, Duration timeout = const Duration(seconds: 10), }) async { try { final result = await _channel.invokeMethod<T>( 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): Promise<void> { 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"的关键设计,但需要注意防止覆盖、内存泄漏和时序竞争问题。

http://www.jsqmd.com/news/1084019/

相关文章:

  • 中医药现代化研究,国自然申请书怎么写才能中?
  • 一台高配置图形工作站带10人SolidWorks画图的实施方案是怎样的
  • 别再浪费Token了!教你用企微回调接口,做个低成本的AI“语料传送带”
  • MusicBee-NeteaseLyrics:让本地音乐库重获网易云音乐歌词生态的智能插件
  • 2026干细胞研究获突破 关键基因机制阐明
  • 加权射影空间中行列式簇的度与正则性计算实战
  • 2026保姆级教程:免费好用手机抠图APP汇总,人像物品一键去除背景全指南
  • 跨平台网盘直链下载解决方案:高效解锁九大网盘文件下载
  • 《HarmonyOS技术精讲-窗口管理》第一篇:窗口基础概念与WindowStage
  • Windows 端口占用排查与释放
  • 用你自己的签名,打你自己
  • 微信会话存档亿级数据处理:基于 RSA 混合解密与 Flink 的流式架构实战
  • 第一次写课程论文不会搭框架?Gradpaper 自动生成标准大纲,跟着填就行
  • 【AI大模型进阶】从GPT-1到GPT-4,它到底进化出了什么“可怕”的能力?
  • C#工业相机开发从零到一:图像采集与显示的工程化实战
  • SDR++:零臃肿的跨平台软件定义无线电软件,你值得拥有吗?
  • 企业为什么要关注智能体?数字化转型关键引擎
  • AI 一天开发一个 APP,为什么最后都死在审核?
  • 公平锁和非公平锁,我学了好几次才记住它们的区别
  • 小红书种草笔记的CES评分机制深度拆解——从算法逻辑到实操提分
  • Python+Selenium自动化测试:Chrome Driver版本管理全流程实现
  • 从CTF实战解析SQL注入:绕过过滤与联合查询攻防
  • 2025年网盘直链下载工具深度解析:LinkSwift如何提升你的下载体验
  • XSS攻击全解析:从原理到防御的Web安全实战指南
  • 6月24日豆包上线专业版!办公任务模式实测惊艳,2亿用户开启AI普惠办公新时代
  • 天行健与优胜劣汰:两种文明范式的哲学比较及其现代启示
  • Java基础进阶:位运算体系与字符串底层原理全解析
  • 如何让老旧Mac焕发新生?OpenCore Legacy Patcher终极指南
  • n8n表达式注入漏洞CVE-2025-68613:从原理到RCE的深度剖析与防御
  • 国产化视频会议安全加密:从国密算法到端到端加密的实战解析