ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

鸿蒙适配踩坑记:Flutter权限申请用permission_handler_ohos解决

鸿蒙适配踩坑记:Flutter权限申请用permission_handler_ohos解决 如果你最近在把 Flutter 应用往鸿蒙设备上迁移八成会踩到同一个坎应用能跑起来一碰权限就露馅。permission_handler 这个在 Android 和 iOS 上都无比顺滑的三方库到了鸿蒙上要么不弹授权框要么权限状态永远返回 denied。我上个月在公司搞扫码 App 的鸿蒙化适配光权限这块就折腾了整整三天最后是接入了 permission_handler_ohos 才彻底解决。这篇文章就把这几天的过程完整复述一遍为什么官方 permission_handler 在鸿蒙上不工作、鸿蒙的权限体系和 Android 到底差在哪、permission_handler_ohos 这套方案怎么接入、动态权限申请的完整代码怎么落地以及我在实际调试中踩过的各种坑。不管是刚开始做鸿蒙化适配、还是已经上手但被权限卡住的朋友都可以对照参考。1. 为什么鸿蒙上不能直接用 permission_handler问题究竟出在哪1.1 鸿蒙与 Android 的权限模型差异很多人第一次接触鸿蒙权限会有一种错觉鸿蒙不是兼容 Android 吗那权限逻辑应该差不多吧实际上差得很多。Android 6.0 之后把权限分成 normal 和 dangerous 两类dangerous 权限需要在运行时通过弹窗申请。鸿蒙则把权限分成 system_grant 和 user_grant 两类system_grant 在应用安装时由系统自动授权user_grant 需要在运行时由用户明确授权。单看分类逻辑两者确实很像但实现机制完全不同。Android 的动态权限申请最终的落点是 Activity 里的 ActivityCompat.requestPermissions 方法回调也是由 Activity 的 onRequestPermissionsResult 接收。鸿蒙的动态权限申请则是基于 UIAbility 的 requestPermissionsFromUser 接口授权结果通过 Promise 或 Callback 返回。对 Flutter 插件来说Android 端插件需要拿到当前 FlutterActivity 的实例去调 Activity 相关 API鸿蒙端则需要拿到 UIAbility 的上下文两边的原生 API 根本不通用。这就是问题的起点permission_handler 的设计是围绕 Android/iOS 的原生能力展开的鸿蒙的权限模型虽然功能上“同构”但底层接口完全独立官方插件自然不会凭空支持一套它没适配过的系统。1.2 permission_handler 的插件机制与鸿蒙的断层permission_handler 在 Flutter 端的运行模式和你写的普通插件没有本质区别Dart 侧通过 MethodChannel 发消息Android/iOS 端各自用原生代码接住消息、调用系统 API再把结果通过 channel 回传。在 Android 端Permission.camera.request() 走到原生代码里就是检查 manifest 声明、判断是否已授权、调 ActivityCompat.requestPermissions、等待 onRequestPermissionsResult 回调最后把 granted 或 denied 的状态塞回 Dart 层。整个过程是一套完整闭环。到了鸿蒙上问题就出现了鸿蒙 Flutter 引擎本身确实支持 PlatformChannel也就是能接收来自 Flutter 的 methodCall但 permission_handler 这个仓库里根本没有鸿蒙端对应的原生实现。Dart 层发出请求之后鸿蒙端没有代码去处理这个 methodCall于是出现两种情况要么直接抛异常要么 channel 返回空结果PermissionStatus 无法解析出一个有效值最终表现就是权限申请没有反应甚至直接崩溃。可以这么理解permission_handler 提供了一把设计精巧的钥匙但这把钥匙只适配了 Android 和 iOS 两把锁鸿蒙这把锁的齿形完全不同钥匙插不进去。1.3 三条适配路径为什么最终选了 permission_handler_ohos既然官方库用不了鸿蒙化适配时有几条路可以走。第一条路自己在 Flutter 侧写一个 MethodChannel再到鸿蒙工程的 EntryAbility 里实现原生权限申请逻辑自己管理回调。这条路不是不能走但权限逻辑本身就涉及状态判断、结果回调、永久拒绝处理这些细节全部自己写的话工作量大而且后续维护成本很高。第二条路在鸿蒙原生侧申请完权限通过 EventChannel 把结果广播回 Flutter 侧。这个方案在简单场景下可行但跨语言、跨线程的回调链路更长一旦权限结果和业务逻辑耦合加深排查问题会非常痛苦。第三条路直接使用 permission_handler_ohos。这个库做的事情很纯粹把 permission_handler 在鸿蒙端的原生实现补齐Dart API 保持完全一致。也就是说业务代码里 import 的还是 package:permission_handler/permission_handler.dart调用方式也和原来一模一样只是底层在鸿蒙上有人接住了 MethodChannel 的消息真正调起了鸿蒙系统的授权框。我在实际项目中选的是第三条路原因也很直接这是对业务代码侵入最小的方案几乎一行业务代码都不用改只要改掉依赖引入方式鸿蒙端就立刻拥有和 Android 一致的权限申请能力。后面所有实战代码都是基于这个方案展开的。2. 接入前必做环境准备与权限声明一步都不能少2.1 鸿蒙 Flutter 工程的目录结构拿到一个能在鸿蒙上跑的 Flutter 工程你会发现它比普通 Flutter 工程多了一个 harmony 目录。这个目录就是用 DevEco Studio 维护的鸿蒙原生工程和 Android 的 android 目录、iOS 的 ios 目录平级。harmony 目录里有几个东西比较关键AppScope 放应用级配置entry 是模块目录里面有一堆源码和配置文件其中最重要的就是 entry/src/main/module.json5。这个文件相当于 Android 的 AndroidManifest.xml权限声明、Ability 配置、元数据都写在里面。还有一个容易忽略的点鸿蒙工程的 Module 名字可能不叫 entry你创建项目时叫什么就是什么。后面配置 usedScene 和定位 Ability 名称时一定要以自己工程里的实际名称为准不要照抄别人博客上的配置。2.2 在 module.json5 中声明动态权限在鸿蒙上凡是 user_grant 类型的权限都必须先在 module.json5 里声明然后才能在代码里动态申请。只写代码不声明运行时会直接失败而且失败方式还很隐蔽常见的就是权限状态永远 denied或者申请时立刻返回失败没有任何弹窗。一个标准的权限声明长这样{ module: { name: entry, type: entry, requestPermissions: [ { name: ohos.permission.CAMERA, reason: $string:permission_reason_camera, usedScene: { abilities: [EntryAbility], when: inuse } } ] } }这里有几个字段必须注意。name 要写完整的权限名比如 ohos.permission.CAMERA不是 CAMERA也不是 android.permission.CAMERA。reason 是申请权限的原因说明对 user_grant 权限是必填项编译时缺了它会直接报错建议配置成字符串资源用 $string:xxx 的方式引用不要硬编码中文文案。usedScene 里 abilities 数组要包含实际申请权限的那个 Ability 名称这个名称和你工程里的 UIAbility 名字严格对应。我有一次就是把 abilities 写成了默认的 MainAbility但工程里实际的 Ability 叫 EntryAbility结果权限声明在部分机型上就是不生效排查了很久才发现是这个字段对不上。2.3 替换依赖实录pubspec.yaml 与 dependency_overrides官方 permission_handler 没有鸿蒙实现所以接入 permission_handler_ohos 的核心思路是在保留原 Dart API 的前提下让鸿蒙端使用 ohos 版本的实现。我见过两种做法。第一种比较粗暴直接把 pubspec.yaml 里的依赖换成 permission_handler_ohosdependencies: permission_handler_ohos: git: url: https://gitee.com/example/permission_handler_ohos.git ref: ohos-12但这样做的代价是代码里原本写的 import package:permission_handler/permission_handler.dart 可能就要改成 import package:permission_handler_ohos/permission_handler.dart如果 ohos 库的导出结构不一样业务代码改动面会很大。第二种做法是用 dependency_overrides保留原依赖不变把实现覆盖成 ohos 分支dependencies: permission_handler: ^11.3.1 dependency_overrides: permission_handler: git: url: https://gitee.com/example/permission_handler.git ref: ohos-main我最终用的是第二种。好处是业务代码里的 import 路径完全不用动pub get 之后直接就能跑。具体仓库地址以你实际使用的维护版本为准但思路是一致的用 dependency_overrides 偷梁换柱。替换完依赖之后记得先 flutter clean 再 flutter pub get然后到 DevEco Studio 里重新 Sync 一次 Flutter 插件。很多适配中的疑难杂症比如原生侧找不到插件注册类、channel 没有响应都是因为没清干净缓存导致的。3. 动态权限申请实战核心代码与权限状态机3.1 单权限申请全流程解析接入完成后的第一件事当然是跑通最简单的一次权限申请。我以一个扫码 App 必定会用的相机权限为例import package:permission_handler/permission_handler.dart; Futurebool requestCameraPermission() async { final status await Permission.camera.request(); switch (status) { case PermissionStatus.granted: return true; case PermissionStatus.denied: return false; case PermissionStatus.permanentlyDenied: // 已经被永久拒绝必须引导用户去系统设置里手动开启 return false; case PermissionStatus.restricted: case PermissionStatus.limited: return false; } }这段代码和你在 Android 上写的没有任何区别这正是选择 permission_handler_ohos 的价值所在。但代码背后发生的事值得搞明白Dart 层执行 request() 时通过 MethodChannel 把“我要申请相机权限”这个消息发给鸿蒙原生侧。permission_handler_ohos 接收到消息后会拿到当前 UIAbility 的上下文调用 requestPermissionsFromUser 拉起系统授权弹窗。用户点击允许或拒绝后系统返回一个 PermissionRequestResult原生代码再把结果转换成 PermissionStatus通过 channel 回传给 await 处。这里有个细节权限申请的结果返回是异步的而且必须发生在 UIAbility 在前台的情况下。如果页面正在处在后台或者弹窗还没关闭就又发起了一次申请鸿蒙系统有可能会直接失败。所以业务侧要避免在极端场景下连续触发权限请求。3.2 多权限批量申请与结果组合判断实际业务里单个权限的场景太少更多是几个权限一起要。比如直播类应用进房间要相机要麦克风还要存储权限。permission_handler 提供了多权限批量申请的能力final permissions [ Permission.camera, Permission.microphone, Permission.photos, ]; final result await Permission.requestPermissions(permissions); final cameraStatus result[Permission.camera]; final micStatus result[Permission.microphone]; final photosStatus result[Permission.photos]; if (cameraStatus.isGranted micStatus.isGranted) { // 进入直播房间 } else { // 提示缺失的权限 }返回的结果是一个 MapPermission, PermissionStatus每个权限对应一个独立的状态。我实测下来鸿蒙系统对多个 user_grant 权限的申请会合并到同一个授权弹窗里展示用户一次性能看到所有权限请求不需要逐个确认。这里要提醒一个容易踩的坑不要用 Map 的遍历顺序去推断弹窗顺序也不要在拿到结果后假设所有权限都成功。正确做法是逐权限取出状态再用 isGranted 逐一判断缺少哪个就提示哪个而不是一刀切。3.3 永久拒绝的识别与用户引导权限申请最怕遇到的情况是用户点了“不允许”还不够还勾选了“不再询问”。在 Android 上这种情况对应 shouldShowRequestPermissionRationale 返回 false之后再次 request 也不会弹窗而是直接返回 denied。鸿蒙上同样存在永久拒绝的概念表现就是 PermissionStatus.permanentlyDenied。碰上永久拒绝最忌讳的事情是继续调用 request()因为这时候怎么调都不会弹窗用户看到的只是权限被静默拒绝体验非常差。正确的处理方式是if (status.isPermanentlyDenied) { // 弹出自定义的引导弹窗 // 提供“去设置”按钮 openAppSettings(); }openAppSettings() 是 permission_handler 提供的 API在鸿蒙适配版本里它的作用是把用户带到应用的“权限管理”设置页相当于把决策权交还给用户。这套逻辑在鸿蒙上没有 Android 那种 rational 状态可以做判断所以更依赖应用自己记录用户的历史拒绝行为配合文案引导给用户一个明确的下一步动作。我在适配时还做了一层保护全局记录每个权限的“申请次数”第 1 次被拒绝后第 2 次触发申请前主动弹自定义说明页而不是再次调系统弹窗这样既不会打扰用户也能把拒绝率压下来。4. 高频权限映射表与典型场景实战4.1 Android 与鸿蒙权限映射对照表鸿蒙化适配过程中最常做的事情就是把 Android 的权限声明翻译成鸿蒙的权限声明。下面是高频权限的映射表直接照抄能省很多事功能场景Android 权限名鸿蒙权限名相机android.permission.CAMERAohos.permission.CAMERA麦克风android.permission.RECORD_AUDIOohos.permission.MICROPHONE精确位置android.permission.ACCESS_FINE_LOCATIONohos.permission.LOCATION模糊位置android.permission.ACCESS_COARSE_LOCATIONohos.permission.APPROXIMATELY_LOCATION读取相册android.permission.READ_MEDIA_IMAGESohos.permission.READ_IMAGEVIDEO写入相册android.permission.WRITE_MEDIA_IMAGES 或 WRITE_EXTERNAL_STORAGEohos.permission.WRITE_IMAGEVIDEO读取日历android.permission.READ_CALENDARohos.permission.READ_CALENDAR写入日历android.permission.WRITE_CALENDARohos.permission.WRITE_CALENDAR读取通讯录android.permission.READ_CONTACTSohos.permission.READ_CONTACTS这张表看着简单实际使用时特别容易翻车。相册权限是最典型的例子Android 在 13 之前用的是 READ_EXTERNAL_STORAGE13 之后才换成 READ_MEDIA_IMAGES而鸿蒙的 READ_IMAGEVIDEO 从 API 上来讲对应的是“读取图片和视频”这个能力。所以迁移的时候不能只看功能名还要看权限的实际含义否则声明了错误权限运行时不弹窗排查起来非常抓狂。4.2 定位权限的特殊处理如果你以为鸿蒙的定位权限就是一个 ohos.permission.LOCATION那肯定会踩坑。鸿蒙把定位拆成了两个权限ohos.permission.LOCATION 是精确位置ohos.permission.APPROXIMATELY_LOCATION 是模糊位置申请精确位置权限时还要求同时声明模糊位置权限。所以在 module.json5 里定位权限的声明一般是这样{ name: ohos.permission.LOCATION, reason: $string:permission_reason_location, usedScene: { abilities: [EntryAbility], when: inuse } }, { name: ohos.permission.APPROXIMATELY_LOCATION, reason: $string:permission_reason_location, usedScene: { abilities: [EntryAbility], when: inuse } }Flutter 侧的申请代码里Permission.location 是 permission_handler 预置的枚举适配版本会把 Android 的 fine 定位映射到鸿蒙这两个权限上所以代码里申请一次即可但声明必须两个都补齐。这个规则和 Android 在 targetSdk 31 之后、申请精确位置必须同时声明模糊位置的逻辑是类似的。4.3 相册和相机场景扫码与选图的权限组合回到我实际做的扫码 App。它的核心链路是这样的扫码需要相机权限扫不出来时用户可以手动点击“从相册选择”这时候又需要读取相册的权限。整个流程如果用代码写出来就是两套权限的组合Futurevoid handleScan() async { final cameraStatus await Permission.camera.request(); if (cameraStatus.isGranted) { startScan(); } else { showPermissionGuide(); } } Futurevoid handleSelectFromAlbum() async { final photosStatus await Permission.photos.request(); if (photosStatus.isGranted) { openImagePicker(); } else { showPermissionGuide(); } }这里有一个交互细节值得注意如果用户先拒绝了相机权限那相册的读取权限也应该缓一缓不要再自动弹出来。我的处理方式是在用户第一次拒绝后记录标记下次进入选择页时先展示自定义说明文案给出按钮让用户主动触发系统弹窗而不是一进入页面就连环弹窗轰炸。这个设计在鸿蒙上尤为重要因为鸿蒙的权限弹窗和 Android 的表现不完全一致连续申请多个权限时系统弹窗会被打断或覆盖体验很难看。5. 常见问题与排查技巧实录5.1 编译期权限声明报错权限声明的编译期报错是大多数人在接入 permission_handler_ohos 时遇到的第一道坎。我把实际遇到过的几类报错整理成了一张速查表报错特征原因解决办法编译报错 reason is requireduser_grant 权限缺少 reason 字段为每个动态权限补上 reason 字符串资源usedScene missing 或 abilities mismatchusedScene 里写的 Ability 名称和工程不匹配改成工程实际的 UIAbility 名称Unknown permission name权限名拼写错误对照官方权限列表检查 ohos.permission.XXX插件重复注册 MethodChannelpermission_handler 和 ohos 实现同时存在清理依赖确保只保留 one 份 channel 实现其中 reason 字段的报错最严格因为鸿蒙编译期做了强制校验缺了直接编不过。我建议在工程里统一建一个 permission_reasons.json把每个权限的申请原因集中管理不要让 reason 散落在各个 module.json5 里后续改文案会方便很多。还有一点不要图省事把所有权限一次性全部写进 requestPermissions这会导致应用安装时的权限“清单”变得很吓人。按需声明、按需申请是鸿蒙上架审核的隐性规则也是用户信任的基础。5.2 运行期不弹窗与回调异常编译通过只是开始运行期的坑更磨人。我实际遇到过的几种情况按出现频率排序第一种是申请权限后完全没有弹窗。这个问题的原因很集中权限已经被永久拒绝了。系统此时不会再次弹窗而是直接返回 denied。处理方式就是前面说的识别 permanentlyDenied引导用户去系统设置手动打开。第二种是应用直接闪退日志里出现类似 E/flutter (31173): [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception 这样的输出。这个报错其实只是 Flutter 运行时遇到了未捕获异常真正的原因在更前面几行日志里。我在适配时遇到的典型原因是原生侧 MethodChannel 没有注册成功Dart 侧调 request() 时找不到 handler抛出了异常。解决办法也很简单clean 掉重新 pub get再在 DevEco Studio 里重新 Sync 构建。第三种是权限回调状态永远等于 denied。十有八九是 module.json5 里没有声明对应权限或者声明名称写错。鸿蒙的权限检查非常严格代码里申请的权限如果没有预先声明系统直接拒绝。5.3 权限状态与 UI 同步的坑结合 Provider 和组件通信权限申请的过程是异步的而且结果会跨页面影响 UI。比如 App 首页有个“开启扫码”按钮用户去设置页开了权限再回来首页的权限提示文案必须立刻更新否则用户会以为设置没生效。我在早期版本里直接在页面回调里 setState结果遇到一个问题申请完权限的那个页面可能已经退出了回调触发后 setState 直接抛异常。后来我把权限状态统一收敛到一个 Provider 模型里用 ChangeNotifier 做单一数据源class PermissionModel extends ChangeNotifier { PermissionStatus _cameraStatus PermissionStatus.denied; PermissionStatus get cameraStatus _cameraStatus; Futurevoid requestCamera() async { _cameraStatus await Permission.camera.request(); notifyListeners(); } void refreshFromSettings() { Permission.camera.status().then((status) { _cameraStatus status; notifyListeners(); }); } }在页面里用 context.watch () 拿到状态无论权限是在哪个页面申请的所有监听这个模型的地方都会自动更新。这个方法也顺带解决了 Flutter 组件通信中常见的变化通知问题不需要手动写 EventBus不需要父组件一层层传回调一个共享模型就搞定了跨页面的状态同步。还有一个容易忽略的细节每次从系统设置页返回 App 时最好重新检查一次权限状态因为用户在设置页的操作不会自动通知 Flutter 应用。鸿蒙的“返回前台”时机可以通过 WidgetsBindingObserver 捕获在 didChangeAppLifecycleState 切到 resumed 时刷新权限状态这个习惯能避免很多稀奇古怪的展示问题。6. 写在最后我在鸿蒙化适配中的几点体会这次适配做下来我最深的一个体会是鸿蒙的权限设计其实比 Android 更规整。Android 因为历史包袱太重权限体系在不同版本之间反复横跳存储权限改了几轮鸿蒙作为后发系统权限模型更统一强制 reason 声明、区分 system_grant 和 user_grant这些约束从工程规范上讲是好事。只要你把 module.json5 的声明做扎实动态权限申请在鸿蒙上反而比 Android 更容易写出确定性强的代码。另一个经验是关于选择适配库的。我一开始也动过自己写 MethodChannel 的念头想着“不就是调用 requestPermissionsFromUser 吗”但真正动手才发现权限状态机、结果映射、永久拒绝识别、多弹窗合并这些细节加起来非常消耗时间。用 permission_handler_ohos 相当于把这块成熟能力直接拿过来用代价只是改一下 pubspec.yaml这笔账怎么算都划算。有一点想特别提醒用 git 引用依赖时不要拉 latest 分支一定锁定 commit 号或 tag。适配库的维护节奏可能很快昨天能用的 commit今天被清理了你的 CI 就会直接挂掉。我后来把依赖固定到了一个确认可用的 commit之后构建就再没因为这个出过问题。最后再分享一个小技巧如果你在鸿蒙设备上调试权限建议同时打开 DevEco Studio 的 Log 面板过滤 ohos.permission 相关关键字鸿蒙系统自己会输出权限校验过程很多时候比 Flutter 侧的日志更能一针见血地告诉你到底哪一步出了问题。把 Android 和鸿蒙两边日志对照着看排查效率能翻倍。
返回列表