鸿蒙 Flutter 项目的权限管理工程化:ArkTS 侧权限申请与 Flutter 侧状态同步的完整方案

适合谁看

  • 正在做 Flutter 鸿蒙项目权限管理的开发者

  • 遇到"权限申请后 Flutter 侧不知道结果"问题的人

  • 想做权限管理工程化封装的开发者

问题背景

在纯 Flutter 项目中,权限管理通常通过 permission_handler 插件完成。但在 Flutter 鸿蒙项目中,权限管理需要在 ArkTS 侧完成,因为:

  1. 鸿蒙的权限系统和 Android/iOS 不同

  2. permission_handler 可能没有鸿蒙适配

  3. 某些权限(如麦克风)需要在 ArkTS 侧直接申请

这意味着 Flutter 侧需要一套机制来感知 ArkTS 侧的权限状态。

项目中的真实场景

食界探味在语音识别功能中需要麦克风权限。权限申请流程:

  1. module.json5 声明 ohos.permission.MICROPHONE

  2. 用户点击麦克风按钮

  3. Flutter 调用 SpeechRecognitionChannel.startListening()

  4. ArkTS 侧 SpeechRecognitionPlugin.requestMicrophonePermission() 弹出权限弹窗

  5. 权限结果通过 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 中使用

  • whenalways 表示始终需要,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 侧权限管理(新增)

鸿蒙侧实现

鸿蒙侧的权限管理涉及三个层次:

  1. 声明层module.json5):静态声明应用需要的权限

  2. 申请层abilityAccessCtrl):运行时弹出权限弹窗

  3. 检查层checkAccessToken):检查权限是否已授予

权限状态流转:

复制代码
未申请 → requestPermissionsFromUser → 已授予/已拒绝/永久拒绝
                                          ↓
                                    checkAccessToken 检查

Flutter 侧实现

Flutter 侧的权限管理策略:

  1. 统一 APIPermissionChannel.checkPermissionrequestPermission

  2. 状态映射 :将 ArkTS 侧的权限状态映射为 Flutter 侧的 PermissionStatus 枚举

  3. 引导策略:根据权限状态决定是否弹出引导对话框

  4. 降级处理:权限被拒绝时提供替代方案

常见坑

  • 坑 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 感知状态并引导用户。关键挑战在于:权限状态的准确映射、永久拒绝的判断、以及用户在系统设置中手动修改权限后的状态同步。