Flutter 3.47 鸿蒙三方库适配实战:permission_handler 权限兼容层
一、项目信息
| 项目 | 内容 |
|---|---|
| 适配仓库名称 | permission_handler |
| 适配仓库地址 | https://atomgit.com/oh-flutter/permission_handler |
| 原始包名 | permission_handler |
| 适配版本 | 13.0.2-ohos.1 |
| 上游项目 | baseflow/flutter-permission-handler |
| 方法通道 | flutter.baseflow.com/permissions/methods |
| OHOS 原生入口 | PermissionHandlerPlugin |
| 构建产物 | entry-default-unsigned.hap |
权限插件适配最容易出现的错误不是“请求失败”,而是“把不支持的权限伪装成已授权”。permission_handler的 OHOS 版本因此采用保守策略:只映射系统有明确对应关系的权限,未映射项统一返回denied。
环境搭建与版本
下面是本次适配实际使用的环境。环境搭建过程这里不再展开,直接看 Flutter OH 环境搭建指南。
| 项目 | 版本/说明 |
|---|---|
| Flutter OH | 3.44.9+ohos-0.0.1-canary1 |
| Dart | 3.12.2 |
| DevEco Studio | 26.0.0 |
| HarmonyOS SDK | API 26 |
| 目标仓库 | permission_handler |
二、保留上游 Dart 契约
业务代码继续使用原来的Permission对象:
finalPermissionStatusstatus=awaitPermission.camera.status;finalPermissionStatusresult=awaitPermission.camera.request();顶层包导出原有 API:
Future<bool>openAppSettings();abstractclassPermission{Future<PermissionStatus>getstatus;Future<PermissionStatus>request();Future<bool>getisGranted;Future<ServiceStatus>getserviceStatus;}OHOS 适配没有要求业务页面新增PermissionOhos类型。只要宿主工程声明了对应 OHOS 权限,Dart 调用就会通过 MethodChannel 进入PermissionHandlerPlugin。
三、权限名称映射
ArkTS 插件使用permissionName()将 Dart 权限枚举转换为 OHOS 权限名:
privatepermissionName(value:number):string|null{switch(value){case1:return'ohos.permission.CAMERA';case2:return'ohos.permission.READ_CONTACTS';case3:case5:return'ohos.permission.APPROXIMATELY_LOCATION';case4:return'ohos.permission.LOCATION_IN_BACKGROUND';case7:case14:return'ohos.permission.MICROPHONE';case13:return'ohos.permission.RECEIVE_SMS';case17:return'ohos.permission.NOTIFICATION_CONTROLLER';case22:case28:case29:case30:return'ohos.permission.DISCOVER_BLUETOOTH';default:returnnull;}}映射时不能只看权限名称相似。例如Permission.locationAlways在 OHOS 上需要后台定位权限,不能复用前台定位权限;蓝牙的扫描、连接和广播能力也要按 OHOS 权限模型分别处理。
四、请求与状态检查
状态检查使用 AccessToken:
privateasynccheckPermission(value:number):Promise<number>{constname=this.permissionName(value);if(name===null||this.context===null){return0;}conststatus=abilityAccessCtrl.createAtManager().checkAccessTokenSync(this.tokenId(),nameasPermissions,);returnstatus===abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED?1:0;}请求权限时逐个调用系统接口:
constresponse=awaitabilityAccessCtrl.createAtManager().requestPermissionsFromUser(this.context,[nameasPermissions]);constgranted=response.authResults.length>0&&response.authResults[0]===abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;returngranted?1:0;这里使用单权限数组是为了让每个Permission的结果保持独立,避免一个权限组的授权结果污染另一个权限。
五、宿主工程必须声明权限
OHOS 权限不能只在 Dart 层请求,宿主module.json5也必须声明。相机和麦克风示例:
"requestPermissions": [ { "name": "ohos.permission.CAMERA", "reason": "$string:camera_permission_reason", "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" } }, { "name": "ohos.permission.MICROPHONE", "reason": "$string:microphone_permission_reason", "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" } } ]对应的string.json:
{ "string": [ { "name": "camera_permission_reason", "value": "用于扫描二维码和拍摄照片" }, { "name": "microphone_permission_reason", "value": "用于录制语音消息" } ] }OHOS 工程目录创建
本次适配前,上游仓库没有ohos/目录。适配分支上按 Flutter OH 的插件工程约束创建平台目录,再把 ArkTS 入口接入pubspec.yaml:
gitswitch-c<your-ohos-adaptation-branch>flutter create--platformsohos.工程生成后至少保留以下边界:
ohos/ ├── index.ets ├── oh-package.json5 └── src/main/ ├── module.json5 └── ets/components/plugin/示例工程还需要生成example/ohos/,并在插件pubspec.yaml的flutter.plugin.platforms.ohos中声明pluginClass;只创建目录、没有注册入口,Dart 方法通道仍然不会工作。
六、当前不支持的能力
openAppSettings()当前明确返回false:
case'openAppSettings':result.success(false);return;shouldShowRequestPermissionRationale()当前返回false,业务应自行在请求前解释权限用途。checkServiceStatus()返回固定的未适用状态,因为 OHOS 没有与旧接口完全一致的服务开关。
这些限制必须让业务代码可见。若把openAppSettings()返回true,调用方会以为已经跳转设置页,最终却看不到任何页面。
七、构建与模拟器验证
验证命令:
flutter pub get flutter analyzecdexample flutter pub get flutter analyze flutter build hap--debug--no-codesign当前结果:
| 检查项 | 结果 |
|---|---|
根目录flutter analyze | 通过 |
示例目录flutter analyze | 通过 |
| OHOS ArkTS 编译 | 通过 |
| HAP 产物 | entry-default-unsigned.hap |
| release HAP 大小 | 约 22 MB |
| 模拟器安装启动 | 通过 |
| 页面状态 | Baseflow 权限列表正常显示 |
适配过程 FAQ 与解决方案
为什么权限状态要通过 AccessToken 检查?
解决方案:Dart 层的枚举只是业务契约,真正的授权状态来自 OHOS AccessToken。只看请求返回值会把“已请求”误判成“已授权”。
为什么请求权限要逐个处理?
解决方案:不同权限的系统弹窗和拒绝语义不同,批量请求会让用户难以理解,也很难区分是哪一个权限导致后续功能失败。适配层应保留逐项状态。
宿主module.json5和插件 HAR 声明为什么不能混为一谈?
解决方案:插件只能声明自身需要的接口;应用是否真正请求和使用权限仍由宿主配置与运行时流程决定。缺少宿主声明时,HAR 编译通过也不代表功能可用。
用户拒绝后,如何区分永久拒绝和临时拒绝?
解决方案:先读取 AccessToken 状态,再根据系统是否还能弹窗决定是否跳转设置页。不能把所有拒绝都直接当作永久拒绝,否则用户会失去再次授权的机会。
八、权限适配的工程化验证
1. 当前映射的完整语义
OHOS 权限名不是简单按字符串替换得到的。当前适配把上游枚举值映射到系统权限:
| 上游枚举值 | OHOS 权限 | 说明 |
|---|---|---|
0,36,37 | ohos.permission.READ_CALENDAR | 日历读取及上游相关变体 |
1 | ohos.permission.CAMERA | 相机 |
2 | ohos.permission.READ_CONTACTS | 联系人读取 |
3,5 | ohos.permission.APPROXIMATELY_LOCATION | 近似位置 |
4 | ohos.permission.LOCATION_IN_BACKGROUND | 后台定位 |
7,14 | ohos.permission.MICROPHONE | 麦克风 |
8 | ohos.permission.READ_CALL_LOG | 通话记录读取 |
9,10 | ohos.permission.READ_IMAGEVIDEO | 图片和视频读取 |
12 | ohos.permission.READ_HIVIEW_DFX | 系统日志读取 |
13 | ohos.permission.RECEIVE_SMS | 接收短信 |
15,23 | ohos.permission.READ_WRITE_USER_FILE | 用户文件读写 |
17 | ohos.permission.NOTIFICATION_CONTROLLER | 通知控制 |
22,28,29,30 | ohos.permission.DISCOVER_BLUETOOTH | 蓝牙发现 |
未出现在表中的权限不会被猜测映射,而是返回PermissionStatus.denied。这是兼容层的安全边界:宁可让业务显示“需要授权”,也不能把未知权限报告为已授权。
2. 状态检查为什么需要 AccessToken
OHOS 的权限状态不能只凭权限名判断,必须知道当前应用的 AccessToken,再交给AtManager.checkAccessTokenSync()查询:
privatetokenId():number{constinfo=bundleManager.getBundleInfoForSelfSync(0);returninfo.appInfo.accessTokenId;}privateasynccheckPermission(value:number):Promise<number>{constname=this.permissionName(value);if(name===null||this.context===null){return0;}conststatus=abilityAccessCtrl.createAtManager().checkAccessTokenSync(this.tokenId(),nameasPermissions,);returnstatus===abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED?1:0;}这里的1和0是通道层的中间值,Dart 平台接口再把它转换为PermissionStatus.granted和PermissionStatus.denied。业务代码不需要知道这个中间编码。
3. 请求权限时为什么逐个请求
requestPermissions接收上游传来的权限枚举列表,然后逐个调用系统请求接口:
for(constpermissionofvalues){output[permission]=awaitthis.requestPermission(permission);}逐个请求的优点是每个权限的结果独立可追踪;缺点是一次请求多个权限时可能出现多个系统弹窗。业务层如果希望一次完成授权,应该继续使用 OHOS 原生权限组能力,而不是依赖当前兼容层的串行实现。
4. 宿主权限声明与插件 HAR 权限声明不能混为一谈
插件 HAR 的module.json5只描述插件模块本身:
{ "module": { "name": "permission_handler", "type": "har", "deviceTypes": ["default", "tablet"] } }真正请求相机、麦克风、定位等权限时,宿主应用的EntryAbility模块必须声明requestPermissions,并提供权限理由。只修改插件 HAR 的module.json5,不会让宿主应用自动获得权限。
5. 业务层如何处理“用户拒绝”
业务不应把request()的返回值直接当成成功或失败布尔值。推荐根据状态分支处理:
Future<void>ensureCameraPermission()async{PermissionStatusstatus=awaitPermission.camera.status;if(!status.isGranted){status=awaitPermission.camera.request();}if(status.isGranted){awaitopenCamera();return;}awaitshowPermissionExplanation('相机权限被拒绝,无法扫描二维码。你可以在系统设置中重新开启。',);}由于当前 OHOS 版本的openAppSettings()返回false,不要在业务里无条件调用openAppSettings()并假设页面已经打开。应先判断返回值,再决定是否显示手动前往设置的提示。
6. 推荐的验证顺序
权限适配最容易在真实设备上出现和单元测试不同的结果,建议按以下顺序验证:
- 未安装权限声明时调用
status,确认不会伪造授权。 - 在宿主
module.json5中声明权限,重新构建 HAP。 - 首次点击
request(),确认系统弹窗出现。 - 拒绝一次后再次请求,观察系统是否仍弹窗。
- 撤销权限后重新启动应用,确认状态回到
denied。 - 对相机、麦克风、定位分别测试,不要用一个权限的结果推断另一个权限。
7. 常见故障排查
| 现象 | 可能原因 | 排查方式 |
|---|---|---|
所有权限都是denied | 宿主未声明权限或枚举未映射 | 检查module.json5和permissionName() |
| 权限弹窗不出现 | 权限理由、usedScene或系统策略配置错误 | 检查宿主配置和模拟器系统设置 |
tokenId()异常 | Bundle 信息不可用或调用时机过早 | 在 Ability 已绑定后再请求 |
openAppSettings()永远false | 当前 OHOS 实现未提供稳定跳转能力 | 业务显示手动设置提示 |
serviceStatus结果不符合预期 | OHOS 没有等价服务开关 | 以notApplicable语义处理 |
| 请求多个权限只看到一个结果 | 权限组或系统弹窗合并 | 逐个验证并记录authResults |
版本与提交基线
| 项目 | 版本/地址 |
|---|---|
| 上游包 | permission_handler |
| 上游最新版本 | 13.0.2 |
| 文章目标基线 | 13.0.2-ohos.1 |
| 当前已发布 TAG | 13.0.2-ohos.1 |
| 适配分支 | main |
| 目标最新 TAG | 13.0.2-ohos.1 |
| 最终提交 | 61f4aa7 |
版本号更新只代表文章和适配分支的基线已对齐;涉及上游 API 变更时,仍需在该 TAG 上重新运行 Dart 测试、ArkTS 编译、HAP 安装和真机回归。
九、总结
权限适配的核心是“语义一致”,而不是“方法返回成功”:
- OHOS 权限名由 Dart 枚举显式映射。
- 宿主
module.json5负责声明权限。 - 未映射权限返回
denied,不伪造授权。 - 设置页和 rationale 能力保持明确限制。
仓库名称:permission_handler
仓库地址:https://atomgit.com/oh-flutter/permission_handler
欢迎加入 CPF-Flutter OpenHarmony 社区,交流 Flutter 三方库适配、权限治理和虚拟机验证经验。