鸿蒙 MICROPHONE 权限在 Flutter 项目里怎么处理
2026/6/13 17:21:58 网站建设 项目流程

适合谁看

  • 第一次在鸿蒙 Flutter 项目里接语音能力的人

  • module.json5声明和运行期权限申请的关系还不清楚的人

  • 想把鸿蒙权限逻辑收进原生插件的人

  • 想理解权限被拒后错误怎么传到 Flutter 页面的人

问题背景

麦克风权限最常见的误区有三个:

  • 误区一:声明了就等于已经授权module.json5里写了ohos.permission.MICROPHONE只是"声明资格",不等于"拿到授权"

  • 误区二:在 Flutter 侧弹一个权限框就够了— Flutter 没有鸿蒙权限的 API,权限申请必须在 ArkTS 层完成

  • 误区三:权限用途文案不重要— 鸿蒙系统要求敏感权限必须配reason字段,否则系统可能直接拒绝弹窗

这三个理解放到鸿蒙 Flutter 项目里都不够准确。鸿蒙的权限模型比 Android 更严格:声明 + 运行期申请 + 用途文案三者缺一不可。

项目中的真实场景

食界探味的语音识别服务于 AI 探味助手,用户"按住说话"时需要麦克风权限。整个权限处理分布在四个文件里:

app/ohos/entry/src/main/module.json5 ← 工程层声明 app/ohos/entry/src/main/resources/base/element/string.json ← 英文用途文案 app/ohos/entry/src/main/resources/zh_CN/element/string.json ← 中文用途文案 app/ohos/entry/src/main/ets/plugins/SpeechRecognitionPlugin.ets ← 运行期申请

这说明权限不是某一个文件的事,而是工程配置、资源文案和插件逻辑三者共同完成

权限链路全景

用户按下"按住说话" │ ▼ Flutter: SpeechRecognitionChannel.startListening() │ ▼ (MethodChannel) 鸿蒙插件: handleStartListening() │ ├──▶ requestMicrophonePermission() ← 运行期申请 │ │ │ ├── 检查 module.json5 是否声明 ← 工程层声明 │ ├── 检查 reason 文案是否存在 ← 资源层文案 │ └── requestPermissionsFromUser ← 弹窗问用户 │ │ │ ├── 授权 ──▶ 继续创建引擎 │ └── 拒绝 ──▶ result.error('PERMISSION_DENIED') │ │ ▼ ▼ Flutter 收到 Future<String> Flutter 收到异常 │ │ ▼ ▼ coordinator.submitQuery(text) coordinator 显示"语音识别出错,请手动输入"

核心实现

一、工程层声明权限

module.json5requestPermissions数组里声明:

{ "name": "ohos.permission.MICROPHONE", "reason": "$string:mic_reason", "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" } }

每个字段的含义:

字段

含义

name

ohos.permission.MICROPHONE

鸿蒙麦克风权限标识

reason

$string:mic_reason

权限用途说明,指向资源文件中的字符串

usedScene.abilities

["EntryAbility"]

哪个 Ability 会使用这个权限

usedScene.when

inuse

权限生效时机:仅使用期间(不是始终)

这一层负责告诉鸿蒙系统:应用有使用麦克风的合法需求,而且只在特定页面、使用期间才需要

为什么when要用inuse而不是always

  • inuse— 应用在前台使用时才需要麦克风,用户更容易接受

  • always— 包括后台也需要,鸿蒙审核更严格,普通语音输入不需要

二、资源层配置权限用途文案

鸿蒙要求敏感权限必须配reason,这个reason指向的是资源文件里的字符串。食界探味在两个语言目录里都配了:

英文(resources/base/element/string.json):

{ "name": "mic_reason", "value": "Used for speech recognition to convert your voice into text" }

中文(resources/zh_CN/element/string.json):

{ "name": "mic_reason", "value": "用于语音识别,将您的语音转换为文字" }

这个文案会出现在鸿蒙系统弹出的权限请求对话框里。如果没配这个字符串,$string:mic_reason解析不出来,鸿蒙系统可能直接拒绝弹窗——用户根本看不到"允许麦克风"的选项。

三、运行期真正申请权限

SpeechRecognitionPlugin.ets里,真正弹窗申请权限的是requestMicrophonePermission()

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

逐行解析:

  1. abilityAccessCtrl.createAtManager()— 创建鸿蒙权限管理器

  2. permissions: Permissions[]— 要申请的权限列表,这里是麦克风

  3. getContext(this)— 获取当前插件的上下文,鸿蒙的权限 API 必须传入 Ability 上下文

  4. requestPermissionsFromUser(context, permissions)— 弹出系统权限对话框,等用户选择

  5. grantResult.authResults.every(...)— 检查每个权限是否都被授权(every是因为可能一次申请多个权限)

工程层声明是"资格预审",运行期申请才是"真正弹窗"。

四、权限被拒后如何中断

handleStartListening里,权限申请是整个流程的第一步:

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; // ← 不会继续创建引擎 } // 第二步之后才会创建引擎、注册监听器、开始识别 try { await this.createEngine(); this.setupListener(); this.startListening(); } catch (err) { // ... } }

关键点:权限不通过时return掉,引擎根本不会被创建。这避免了在无权限状态下浪费系统资源。

五、权限逻辑留在鸿蒙原生层

食界探味没有把权限申请放在 Flutter 页面里,而是放在鸿蒙语音识别插件内部。这么做的好处是:

  • 调用方不需要先写权限前置逻辑— Flutter 侧startListening()一行代码搞定,不需要先requestPermission()startListening()

  • 语音识别入口天然自带权限保护— 任何地方调startListening都会经过权限检查,不存在"忘加权限"的情况

  • 后续换页面调用时不会遗漏— 如果有第二个页面也需要语音输入,直接调 channel 就行,权限逻辑不会重复

关键代码位置

  • app/ohos/entry/src/main/module.json5ohos.permission.MICROPHONE声明

  • app/ohos/entry/src/main/resources/base/element/string.json— 英文权限用途文案

  • app/ohos/entry/src/main/resources/zh_CN/element/string.json— 中文权限用途文案

  • app/ohos/entry/src/main/ets/plugins/SpeechRecognitionPlugin.ets— 运行期权限申请逻辑

  • app/lib/core/platform/speech_recognition_channel.dart— Flutter 侧 Channel 封装

  • app/lib/core/ai/ai_explore_coordinator.dart— Flutter 侧权限拒绝后的错误处理

鸿蒙侧实现

鸿蒙侧的权限处理建议固定成一条顺序:

1. module.json5 声明权限 + reason 文案 2. string.json 里写中英文权限用途说明 3. 插件 handleStartListening 里调 requestPermissionsFromUser 4. 拒绝时 result.error('PERMISSION_DENIED'),直接 return 5. 通过后才创建引擎、注册监听器、开始识别

这套顺序的核心思路是:权限是识别链路的守门人,不是可选步骤

鸿蒙权限 vs Android 权限

维度

鸿蒙

Android

工程层声明

module.json5requestPermissions

AndroidManifest.xml<uses-permission>

运行期申请

atManager.requestPermissionsFromUser()

ActivityCompat.requestPermissions()

用途文案

必须配reason字段,否则可能不弹窗

可选,不影响弹窗

生效时机

when: inuse/always

maxSdkVersion等控制

权限管理器

abilityAccessCtrl.createAtManager()

ActivityCompat/ContextCompat

可以看到鸿蒙的权限模型整体比 Android 更严格,尤其是reason文案是硬性要求。

Flutter 侧实现

Flutter 侧最好的做法反而是"少做事":

class SpeechRecognitionChannel { static const _channel = MethodChannel('com.foodvoyage.speech_recognition'); static Future<String> startListening({String language = 'zh-CN'}) async { final result = await _channel.invokeMethod<String>( 'startListening', {'language': language}, ); return result ?? ''; } }

Flutter 侧完全不需要感知鸿蒙权限的存在。如果权限被拒,鸿蒙插件会返回error('PERMISSION_DENIED', ...),Flutter 侧的invokeMethod会抛出PlatformException,由协调器统一捕获:

Future<void> startVoiceInput() async { if (!mounted) return; state = state.copyWith( status: AiSessionStatus.listening, errorMessage: null, ); try { final text = await SpeechRecognitionChannel.startListening(); // ... 正常流程 } catch (e) { // 权限被拒、引擎创建失败、识别超时等异常都在这里统一兜底 AppLogger.error('[AI助手] 语音识别出错: $e'); if (!mounted) return; state = state.copyWith( status: AiSessionStatus.error, errorMessage: '语音识别出错,请手动输入', ); } }

不管鸿蒙侧返回的是PERMISSION_DENIED还是ASR_ERROR,Flutter 协调器都统一处理为"语音识别出错,请手动输入",然后降级到文字输入。用户不需要知道具体是哪个环节出了问题,只需要知道"语音不行了,可以打字"。

这就是跨端权限设计的价值:鸿蒙侧管权限细节,Flutter 侧管用户体验

常见坑

  • module.json5里声明了权限,但没有运行期申请— 鸿蒙系统会直接拒绝麦克风访问,不弹窗,用户完全无感

  • 运行期申请写了,但module.json5没声明requestPermissionsFromUser会直接报错,因为系统不知道你有这个权限资格

  • 配了reason指向$string:mic_reason,但string.json里没有这个 key— 鸿蒙解析不到文案,系统可能拒绝弹窗

  • 只配了英文文案,没配zh_CN文案— 中文用户的权限弹窗可能显示 key 名而不是说明文字

  • 运行期申请写在 Flutter 页面层— 导致多个入口重复申请,而且 Flutter 没有鸿蒙权限 API,根本写不了

  • 拒绝权限后仍然继续创建识别引擎— 引擎在无权限状态下创建会失败或行为异常,浪费时间又增加错误处理复杂度

  • 权限被拒时没清理pendingResultpendingResult保留了悬挂的MethodResult,后续可能被重复调用

可复用模板

鸿蒙 module.json5 权限声明

"requestPermissions": [ { "name": "ohos.permission.MICROPHONE", "reason": "$string:mic_reason", "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" } } ]

鸿蒙 string.json 权限文案

// resources/base/element/string.json (英文/默认) { "name": "mic_reason", "value": "Used for speech recognition to convert your voice into text" } // resources/zh_CN/element/string.json (中文) { "name": "mic_reason", "value": "用于语音识别,将您的语音转换为文字" }

鸿蒙运行期权限申请(ArkTS)

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

鸿蒙插件中权限拒绝后的中断模板

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; // ← 直接中断,不创建引擎 } // 权限通过后才继续... await this.createEngine(); this.setupListener(); this.startListening(); }

Flutter 侧权限异常的统一兜底

try { final text = await SpeechRecognitionChannel.startListening(); // 正常流程... } catch (e) { // 权限被拒、引擎失败等所有异常统一兜底 state = state.copyWith( status: AiSessionStatus.error, errorMessage: '语音识别出错,请手动输入', ); }

本篇总结

  • 鸿蒙麦克风权限至少有三层module.json5声明、string.json用途文案、ArkTS 运行期申请,缺任何一层都可能导致权限流程断裂

  • 鸿蒙的权限模型比 Android 更严格:reason文案是硬性要求,不配可能直接不弹窗

  • 权限逻辑应该收进鸿蒙原生插件内部(handleStartListening的第一步),让 Flutter 侧完全不感知权限细节

  • 权限被拒后的错误通过PlatformException传到 Flutter,由协调器统一降级为"请手动输入"

  • when: "inuse""always"更适合语音输入场景,用户更容易接受

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询