鸿蒙 MICROPHONE 权限在 Flutter 项目里怎么处理
适合谁看
-
第一次在鸿蒙 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.json5 的 requestPermissions 数组里声明:
{
"name": "ohos.permission.MICROPHONE",
"reason": "$string:mic_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
每个字段的含义:
|
字段 |
值 |
含义 |
|---|---|---|
|
|
|
鸿蒙麦克风权限标识 |
|
|
|
权限用途说明,指向资源文件中的字符串 |
|
|
|
哪个 Ability 会使用这个权限 |
|
|
|
权限生效时机:仅使用期间(不是始终) |
这一层负责告诉鸿蒙系统:应用有使用麦克风的合法需求,而且只在特定页面、使用期间才需要。
为什么 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;
}
}
逐行解析:
-
abilityAccessCtrl.createAtManager()— 创建鸿蒙权限管理器 -
permissions: Permissions[]— 要申请的权限列表,这里是麦克风 -
getContext(this)— 获取当前插件的上下文,鸿蒙的权限 API 必须传入 Ability 上下文 -
requestPermissionsFromUser(context, permissions)— 弹出系统权限对话框,等用户选择 -
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.json5—ohos.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 |
|---|---|---|
|
工程层声明 |
|
|
|
运行期申请 |
|
|
|
用途文案 |
必须配 |
可选,不影响弹窗 |
|
生效时机 |
|
|
|
权限管理器 |
|
|
可以看到鸿蒙的权限模型整体比 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,根本写不了
-
拒绝权限后仍然继续创建识别引擎 — 引擎在无权限状态下创建会失败或行为异常,浪费时间又增加错误处理复杂度
-
权限被拒时没清理
pendingResult—pendingResult保留了悬挂的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"更适合语音输入场景,用户更容易接受
更多推荐



所有评论(0)