【Flutter】封装一个兼容Android+iOS的动态权限与多媒体权限的工具

2,637 阅读9分钟

Flutter中Android与iOS的权限申请与校验封装

前言

在前文中我们讲到过 Android 的权限收紧过程,以及如何访问到多媒体权限和SD卡权限,而 iOS 同样分为外置储存,一般为 DocumentsDownloads 文件夹,以及如 com.apple.security.assets.movies.read-writecom.apple.security.assets.music.read-write 之类的多媒体权限。

可以大致看出其实 Android 和 iOS 的多媒体权限,以及其他的储存权限,定位权限,相机权限等等都越来越趋同了。那么在 Fultter 的移动端应用中,我们应该怎么去管理以及申请这些权限呢?

permission_handler 很好,看来大家都会,但是如果只是 permission_handler 可以解决一部分动态权限申请,但是多媒体的权限我们同样需要另一个插件来管理 photo_manager 确实是方便了。

那么我们应该如何分别管理 Android 和 iOS 的申请权限,以及如何统一申请的流程,要知道 iOS 是需要在配置中声明权限说明,而 Android 并不需要,只是在部分应用市场上架的时候才会提示你不符合规范(华为应用市场)。到头来我们也是需要处理权限申请说明,在比较流行的 Android 动态权限申请框架如 XXPermission 中就默认自带了动态权限申请的说明,那么在 Flutter 的开发中我们总不至于再去单独导入一个 Android 库吧,那我应该怎么办?

这也是本文的重点,我们一步步来处理统一双端的权限处理流程。

话不多说,Let's go

300.png

一、导入依赖与权限处理

根据上文所说,我们先导入 permission_handlerphoto_manager 插件,如果你导入了一些第三方的图库相册插件的话,可能他们已经默认导入了这些插件,大家需要注意。

并且一些第三方的图库插件内部自带了多媒体权限申请,但是这肯定是不符合部分应用市场的需求的哇,所以最好还是我们自己先封装权限申请引擎,自己申请了权限之后再跳转到第三方的图库。

对于多媒体的权限,我们使用 PhotoManager.requestPermissionExtend() 方法来触发,但是也需要注意双端平台的差异。

requestPermissionExtend 是一个异步的任务,它能查询 iOS 的多媒体权限,并且申请多媒体的权限,但是对于 Andorid 平台只能申请权限,对于查询权限有兼容性问题,所以我们需要使用 Permission 插件的 Permission.storage.status 来判断是否申请了权限。

除了多媒体权限之外,我们的其他动态权限倒是简单了,比如我们申请相机权限

 var status = await Permission.camera.status
  if (status.isGranted) {
  }else{

  }

我们可以使用 status 的方案去查询状态,然后使用

 var permissionRequestFuture = Permission.camera.request();

的 request 的方案去申请选项,这一点倒是方便简单。

那么如果想要在 Android 端实现类似 iOS 的权限说明弹窗(或华为应用商城的权限效果)我们就需要处理弹窗与申请的 Future 问题。

我这里我的做法是多个 Future 并发判断,如果500毫秒没有自动申请成功才会弹窗,避免了每一次校验都会弹窗的问题。

大致代码如下:

      // 未授权,则准备发起一次申请
        var permissionRequestFuture = PhotoManager.requestPermissionExtend();

        // 延迟500毫秒的Future
        var delayFuture = Future.delayed(Duration(milliseconds: 500), () => 'delay');

        // 使用Future.any等待上述两个Future中的任何一个完成
        var firstCompleted = await Future.any([permissionRequestFuture, delayFuture]);

        // 判断响应结果
        if (firstCompleted == 'delay') {
          Log.d("判断响应结果:1");
          // 如果是延迟Future完成了,表示500毫秒内没有获得权限响应,显示对话框
          _showPermissionDialog("“乐活儿”想访问你的多媒体相册 用于图片上传,图片保存等功能,请允许我获取您的权限");
          // 再次等待权限请求结果
          ps = await permissionRequestFuture;
          DialogEngine.dismiss(tag: "permission");
        } else {
          Log.d("判断响应结果:2");
          // 权限请求已完成,立刻取消对话框展示(如果已经展示的话)
          DialogEngine.dismiss(tag: "permission");
          ps = firstCompleted as PermissionState;
        }

具体的流程是,先校验设备是否有对应的权限,如果没有权限,尝试申请权限,申请权限分为正常的申请 Future 和500毫秒的延迟 Future ,如果申请权限的校验直接通过就不需要弹窗说明权限,如果没有通过则需要展示弹窗说明以及对应的系统的动态权限申明了。

二、封装实现

那么完成的代码如下:

/**
 * 动态权限的申请与校验
 */
class PermissionEngine {
  // 私有构造函数
  PermissionEngine._privateConstructor();

  // 单例实例
  static final PermissionEngine _instance = PermissionEngine._privateConstructor();

  // 获取单例实例的访问点
  factory PermissionEngine() {
    return _instance;
  }

  /// 申请多媒体相册权限
  void requestPhotosPermission(void Function() success) async {
    //相册的选项
    if (DeviceUtils.isIOS) {
      //申请授权
      final value = await PhotoManager.requestPermissionExtend();
      if (value.hasAccess) {
        //已授权
        Log.d("相册已授权");
        success();
      } else if (value == PermissionState.limited) {
        Log.d("相册访问受限,去设置受限");
        PhotoManager.presentLimited();
      } else {
        Log.d("相册无授权,去设置");
        DialogEngine.show(
          widget: AppDefaultDialog(
            "无相册权限,前往设置",
            confirmAction: () {
              PhotoManager.openSetting();
            },
          ),
        );
      }
    } else {
      //Android是否有SD卡权限
      var status = await Permission.storage.status;
      late PermissionState ps;
      if (status.isGranted) {
        // 已经授权
        success();
      } else {
        // 未授权,则准备发起一次申请
        var permissionRequestFuture = PhotoManager.requestPermissionExtend();

        // 延迟500毫秒的Future
        var delayFuture = Future.delayed(Duration(milliseconds: 500), () => 'delay');

        // 使用Future.any等待上述两个Future中的任何一个完成
        var firstCompleted = await Future.any([permissionRequestFuture, delayFuture]);

        // 判断响应结果
        if (firstCompleted == 'delay') {
          Log.d("判断响应结果:1");
          // 如果是延迟Future完成了,表示500毫秒内没有获得权限响应,显示对话框
          _showPermissionDialog("“Newki爱学习”想访问你的多媒体相册 用于图片上传,图片保存等功能,请允许我获取您的权限");
          // 再次等待权限请求结果
          ps = await permissionRequestFuture;
          DialogEngine.dismiss(tag: "permission");
        } else {
          Log.d("判断响应结果:2");
          // 权限请求已完成,立刻取消对话框展示(如果已经展示的话)
          DialogEngine.dismiss(tag: "permission");
          ps = firstCompleted as PermissionState;
        }

        if (ps.isAuth) {
          // 用户授权
          success();
        } else {
          // 权限被拒绝
          await DialogEngine.show(
            widget: AppDefaultDialog("请到您的手机设置打开相册的权限", title: "提醒", confirmText: "去设置", confirmAction: () {
              openAppSettings();
            }),
          );
        }
      }
    }
  }

  /// 申请相机权限
  void requestCameraPermission(void Function() success) async {
    // 获取当前的权限
    var status = await Permission.camera.status;
    if (status.isGranted) {
      // 已经授权
      success();
    } else {
      // 未授权,则准备发起一次申请
      var permissionRequestFuture = Permission.camera.request();

      // 延迟500毫秒的Future
      var delayFuture = Future.delayed(Duration(milliseconds: 500), () => 'delay');

      // 使用Future.any等待上述两个Future中的任何一个完成
      var firstCompleted = await Future.any([permissionRequestFuture, delayFuture]);

      // 判断响应结果
      if (firstCompleted == 'delay') {
        // 如果是延迟Future完成了,表示500毫秒内没有获得权限响应,显示对话框
        _showPermissionDialog("“Newki爱学习”申请调用您的相机权限 用于使用拍摄头像,图片上传保存等功能,请允许我获取您的权限");
        // 再次等待权限请求结果
        status = await permissionRequestFuture;
        DialogEngine.dismiss(tag: "permission");
      } else {
        // 权限请求已完成,立刻取消对话框展示(如果已经展示的话)
        DialogEngine.dismiss(tag: "permission");
        status = firstCompleted as PermissionStatus;
      }

      if (status.isGranted) {
        // 用户授权
        success();
      } else {
        // 权限被拒绝
        await DialogEngine.show(
          widget: AppDefaultDialog("请到您的手机设置打开相机的权限", title: "提醒", confirmText: "去设置", confirmAction: () {
            openAppSettings();
          }),
        );
      }
    }
  }

  /// 校验并申请定位权限
  Future<bool> requestLocationPermission() async {
    // 获取当前的权限
    var status = await Permission.location.status;
    if (status.isGranted) {
      // 已经授权
      return true;
    } else {
      // 未授权,则准备发起一次申请
      var permissionRequestFuture = Permission.location.request();

      // 延迟500毫秒的Future
      var delayFuture = Future.delayed(Duration(milliseconds: 500), () => 'delay');

      // 使用Future.any等待上述两个Future中的任何一个完成
      var firstCompleted = await Future.any([permissionRequestFuture, delayFuture]);

      // 判断响应结果
      if (firstCompleted == 'delay') {
        // 如果是延迟Future完成了,表示500毫秒内没有获得权限响应,显示对话框
        _showPermissionDialog("“Newki爱学习”想访问您的定位权限获取您的位置来推荐附近的工作");
        // 再次等待权限请求结果
        status = await permissionRequestFuture;
        DialogEngine.dismiss(tag: "permission");
      } else {
        Log.d("权限请求已完成,立刻取消对话框展示");
        // 权限请求已完成,立刻取消对话框展示(如果已经展示的话)
        DialogEngine.dismiss(tag: "permission");
        status = firstCompleted as PermissionStatus;
      }

      if (status.isGranted) {
        // 用户授权
        return true;
      } else {
        // 权限被拒绝
        await DialogEngine.show(
          widget: AppDefaultDialog("请到您的手机设置打开定位的权限", title: "提醒", confirmText: "去设置", confirmAction: () {
            openAppSettings();
          }),
        );
        return false;
      }
    }
  }

  //顶部展示权限声明详情弹窗
  void _showPermissionDialog(String desc) {
    DialogEngine.show(
      clickMaskDismiss: false,
      backDismiss: true,
      tag: "permission",
      maskColor: Colors.transparent,
      widget: PermissionDescDialog(desc),
    );
  }
}

我个人就只是简单的写了下多媒体权限与相机定位权限,大家完全可以自己写其他的权限申请或者封装为参数让上层传入都行。只是我自己是封装的引擎类,在组件化中上层应用隔离了权限相关的依赖库所以才用的方法区分。

至于 DialogEngine 大家也不需要有疑问,其实就是弹窗的引擎封装,弹出具体的权限声明的信息框,大家完全可以自己定义并且能自己定义样式。

多媒体权限效果图:

20240620_172048.gif

已有权限不会再次弹窗:

20240620_172434.gif

相机权限:

20240620_172114.gif

后记

我们简单的说了如何申请与校验多媒体权限,以及 Permission 插件的简单校验与申请。

其次我们对于弹窗的权限说明做了封装,聚合完全的逻辑并给出了完整的代码,对于重复的权限申请与弹窗说明做出限制,并且对于权限拒绝之后的弹窗指引用户进入设置页面打开对应的权限管理。

虽然是一个很小的功能但是也是很常用的功能,我们做好动态权限与多媒体权限的封装之后,就能保持和 iOS 一致的处理流程,从而实现特殊应用市场的上架需求。

本文的代码其实很简单已经在文中全部贴出,当然如果你有其他的更多的更好的实现方式,也希望大家能评论区交流一起学习进步。如果我的文章有错别字,不通顺的,或者代码、注释、有错漏的地方,同学们都可以指出修正。

如果感觉本文对你有一点的启发和帮助,还望你能点赞支持一下,你的支持对我真的很重要。

Ok,这一期就此完结。