适合谁看
-
正在做 Flutter 鸿蒙项目权限管理的开发者
-
遇到"权限申请后 Flutter 侧不知道结果"问题的人
-
想做权限管理工程化封装的开发者
问题背景
在纯 Flutter 项目中,权限管理通常通过 permission_handler 插件完成。但在 Flutter 鸿蒙项目中,权限管理需要在 ArkTS 侧完成,因为:
-
鸿蒙的权限系统和 Android/iOS 不同
-
permission_handler可能没有鸿蒙适配 -
某些权限(如麦克风)需要在 ArkTS 侧直接申请
这意味着 Flutter 侧需要一套机制来感知 ArkTS 侧的权限状态。
项目中的真实场景
食界探味在语音识别功能中需要麦克风权限。权限申请流程:
-
module.json5声明ohos.permission.MICROPHONE -
用户点击麦克风按钮
-
Flutter 调用
SpeechRecognitionChannel.startListening() -
ArkTS 侧
SpeechRecognitionPlugin.requestMicrophonePermission()弹出权限弹窗 -
权限结果通过
MethodResult回传到 Flutter
核心实现
第一层:module.json5 权限声明
// app/ohos/entry/src/main/module.json5
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.MICROPHONE",
"reason": "$string:permission_mic_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "always"
}
}
]
}
}
权限声明的关键点:
-
name:权限的全限定名 -
reason:权限申请理由,会显示在权限弹窗中 -
usedScene:说明权限在哪些 Ability 中使用 -
when:always表示始终需要,inuse表示仅使用时需要
第二层:ArkTS 侧运行时申请
// 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;
}
}
requestPermissionsFromUser 返回值结构:
{
authResults: number[]; // 每个权限的授权结果
permissions: string[]; // 请求的权限列表
wantingIndex: number; // 用户操作的权限索引
}
authResults 中的值:
-
PERMISSION_GRANTED(0):已授权 -
PERMISSION_DENIED(1):已拒绝 -
PERMISSION_NEVER_ASK(2):永久拒绝(用户勾选了"不再询问")
第三层:Flutter 侧统一权限管理
// core/platform/permission_channel.dart
class PermissionChannel {
static const _channel = MethodChannel('com.foodvoyage.permission');
/// 检查权限状态
static Future<PermissionStatus> checkPermission(String permission) async {
try {
final result = await _channel.invokeMethod<String>('checkPermission', {
'permission': permission,
});
return _parseStatus(result);
} on MissingPluginException {
return PermissionStatus.denied;
}
}
/// 请求权限
static Future<PermissionStatus> requestPermission(String permission) async {
try {
final result = await _channel.invokeMethod<String>('requestPermission', {
'permission': permission,
});
return _parseStatus(result);
} on MissingPluginException {
return PermissionStatus.denied;
}
}
static PermissionStatus _parseStatus(String? result) {
switch (result) {
case 'granted':
return PermissionStatus.granted;
case 'denied':
return PermissionStatus.denied;
case 'permanentlyDenied':
return PermissionStatus.permanentlyDenied;
default:
return PermissionStatus.denied;
}
}
}
enum PermissionStatus {
granted,
denied,
permanentlyDenied,
}
第四层:ArkTS 侧权限检查与请求
// plugins/PermissionPlugin.ets
export default class PermissionPlugin implements FlutterPlugin, MethodCallHandler {
private channel: MethodChannel | null = null;
onAttachedToEngine(binding: FlutterPluginBinding): void {
this.channel = new MethodChannel(binding.getBinaryMessenger(), 'com.foodvoyage.permission');
this.channel.setMethodCallHandler(this);
}
onMethodCall(call: MethodCall, result: MethodResult): void {
switch (call.method) {
case 'checkPermission':
this.handleCheckPermission(call, result);
break;
case 'requestPermission':
this.handleRequestPermission(call, result);
break;
}
}
private async handleCheckPermission(call: MethodCall, result: MethodResult): Promise<void> {
const permission = call.arguments['permission'] as string;
try {
const atManager = abilityAccessCtrl.createAtManager();
const context = getContext(this);
const status = await atManager.checkAccessToken(context, permission);
result.success(status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
? 'granted' : 'denied');
} catch (err) {
result.success('denied');
}
}
private async handleRequestPermission(call: MethodCall, result: MethodResult): Promise<void> {
const permission = call.arguments['permission'] as string;
try {
const atManager = abilityAccessCtrl.createAtManager();
const context = getContext(this);
const permissions: Permissions[] = [permission as Permissions];
const grantResult = await atManager.requestPermissionsFromUser(context, permissions);
const status = grantResult.authResults[0];
if (status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
result.success('granted');
} else if (status === 2) { // NEVER_ASK
result.success('permanentlyDenied');
} else {
result.success('denied');
}
} catch (err) {
result.success('denied');
}
}
}
第五层:Flutter 侧权限引导
// 权限引导对话件
class PermissionGuideDialog extends StatelessWidget {
final String permissionName;
final VoidCallback onOpenSettings;
const PermissionGuideDialog({
required this.permissionName,
required this.onOpenSettings,
});
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('需要$permissionName权限'),
content: Text('请在系统设置中开启$permissionName权限,以使用相关功能。'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('取消'),
),
TextButton(
onPressed: () {
Navigator.pop(context);
onOpenSettings();
},
child: Text('去设置'),
),
],
);
}
}
// 使用示例
Future<void> requestMicPermission(BuildContext context) async {
final status = await PermissionChannel.requestPermission('ohos.permission.MICROPHONE');
if (status == PermissionStatus.granted) {
// 权限已授予,继续操作
return;
}
if (status == PermissionStatus.permanentlyDenied) {
// 永久拒绝,引导用户去系统设置
if (context.mounted) {
showDialog(
context: context,
builder: (context) => PermissionGuideDialog(
permissionName: '麦克风',
onOpenSettings: () {
// 打开系统设置
},
),
);
}
return;
}
// 普通拒绝
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('需要麦克风权限才能使用语音功能')),
);
}
}
关键代码位置
-
app/ohos/entry/src/main/module.json5--- 权限声明 -
app/ohos/entry/src/main/ets/plugins/SpeechRecognitionPlugin.ets:84-95--- 麦克风权限申请 -
app/ohos/entry/src/main/ets/plugins/PermissionPlugin.ets--- 通用权限管理插件(新增) -
app/lib/core/platform/permission_channel.dart--- Flutter 侧权限管理(新增)
鸿蒙侧实现
鸿蒙侧的权限管理涉及三个层次:
-
声明层 (
module.json5):静态声明应用需要的权限 -
申请层 (
abilityAccessCtrl):运行时弹出权限弹窗 -
检查层 (
checkAccessToken):检查权限是否已授予
权限状态流转:
未申请 → requestPermissionsFromUser → 已授予/已拒绝/永久拒绝
↓
checkAccessToken 检查
Flutter 侧实现
Flutter 侧的权限管理策略:
-
统一 API :
PermissionChannel.checkPermission和requestPermission -
状态映射 :将 ArkTS 侧的权限状态映射为 Flutter 侧的
PermissionStatus枚举 -
引导策略:根据权限状态决定是否弹出引导对话框
-
降级处理:权限被拒绝时提供替代方案
常见坑
-
坑 1:module.json5 声明的权限和运行时申请的权限不一致。声明了 A 权限但运行时申请 B 权限,系统会拒绝。
-
坑 2:
requestPermissionsFromUser的authResults数组长度和请求的权限数量不一致。如果请求了多个权限,需要逐一检查每个结果。 -
坑 3:永久拒绝状态的判断不准确。不同鸿蒙版本对"永久拒绝"的定义可能不同,需要做兼容性处理。
-
坑 4:权限弹窗被系统静默拒绝。某些权限(如后台定位)可能需要额外的配置才能弹出权限弹窗。
-
坑 5:Flutter 侧不知道权限状态变化。如果用户在系统设置中手动修改权限,Flutter 侧不会收到通知。需要定期检查或监听权限变化。
可复用模板
// Flutter 侧 - 权限管理封装模板
class PermissionHelper {
static const _channel = MethodChannel('com.example.permission');
static Future<bool> requestWithGuide(
BuildContext context, {
required String permission,
required String permissionName,
required VoidCallback onGranted,
}) async {
final status = await _requestPermission(permission);
if (status == 'granted') {
onGranted();
return true;
}
if (status == 'permanentlyDenied' && context.mounted) {
_showGuideDialog(context, permissionName);
}
return false;
}
static Future<String> _requestPermission(String permission) async {
try {
return await _channel.invokeMethod<String>('requestPermission', {
'permission': permission,
}) ?? 'denied';
} on MissingPluginException {
return 'denied';
}
}
static void _showGuideDialog(BuildContext context, String name) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('需要$name权限'),
content: Text('请在系统设置中开启$name权限'),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: Text('取消')),
TextButton(onPressed: () => Navigator.pop(context), child: Text('去设置')),
],
),
);
}
}
// 鸿蒙侧 - 权限管理插件模板
export default class PermissionPlugin implements FlutterPlugin, MethodCallHandler {
private channel: MethodChannel | null = null;
onAttachedToEngine(binding: FlutterPluginBinding): void {
this.channel = new MethodChannel(binding.getBinaryMessenger(), 'com.example.permission');
this.channel.setMethodCallHandler(this);
}
onMethodCall(call: MethodCall, result: MethodResult): void {
if (call.method === 'requestPermission') {
const permission = call.arguments['permission'] as string;
this.requestPermission(permission, result);
}
}
private async requestPermission(permission: string, result: MethodResult): Promise<void> {
try {
const atManager = abilityAccessCtrl.createAtManager();
const context = getContext(this);
const grantResult = await atManager.requestPermissionsFromUser(
context, [permission as Permissions]
);
const status = grantResult.authResults[0];
if (status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
result.success('granted');
} else if (status === 2) {
result.success('permanentlyDenied');
} else {
result.success('denied');
}
} catch (err) {
result.success('denied');
}
}
}
本篇总结
鸿蒙 Flutter 项目的权限管理工程化,核心是三层协作:module.json5 声明权限 → ArkTS 侧 abilityAccessCtrl 运行时申请 → Flutter 侧 PermissionChannel 感知状态并引导用户。关键挑战在于:权限状态的准确映射、永久拒绝的判断、以及用户在系统设置中手动修改权限后的状态同步。
