统一管理权限请求:permission_handler

permission_handler的主要作用是 跨平台(Android/iOS/macOS/Windows/Linux/web)统一管理权限请求,避免开发者直接去写平台特定代码。

入门案例

安装

flutter pub add permission_handler 

配置

安卓配置

android/app/src/main/AndroidManifest.xml:在该配置文件下作如下配置:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
  <uses-permission android:name="android.permission.CAMERA"/>
</manifest>

IOS的配置

ios/Runner/Info.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
	<dict>
    <key>NSCameraUsageDescription</key>
		<string>需要相机权限来拍照</string>
  </dict>
</plist>

ios/Podfile

# 注释掉原来的 post_install
# post_install do |installer|
#   installer.pods_project.targets.each do |target|
#     flutter_additional_ios_build_settings(target)
#   end
# end

post_install do |installer|
  installer.pods_project.targets.each do |target|
    flutter_additional_ios_build_settings(target)

    target.build_configurations.each do |config|
      # You can remove unused permissions here
      # for more information: https://github.com/BaseflowIT/flutter-permission-handler/blob/master/permission_handler/ios/Classes/PermissionHandlerEnums.h
      # e.g. when you don't need camera permission, just add 'PERMISSION_CAMERA=0'
      config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
        '$(inherited)',
        ## dart: PermissionGroup.camera
        'PERMISSION_CAMERA=1',
      ]
    end
  end
end

代码

import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Permission Demo',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const PermissionExample(),
    );
  }
}

class PermissionExample extends StatefulWidget {
  const PermissionExample({super.key});

  @override
  State<PermissionExample> createState() => _PermissionExampleState();
}

class _PermissionExampleState extends State<PermissionExample> {
  String _status = "未知";

  Future<void> _checkCameraPermission() async {
    // 请求相机权限
    PermissionStatus status = await Permission.camera.request();

    setState(() {
      if (status.isGranted) {
        _status = "相机权限已获取 ✅";
      } else if (status.isDenied) {
        _status = "相机权限被拒绝 ❌";
      } else if (status.isPermanentlyDenied) {
        _status = "相机权限永久拒绝,需要去设置开启 ⚠️";
      } else {
        _status = "相机权限状态: $status";
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("Permission Demo")),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(_status, style: const TextStyle(fontSize: 18)),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: _checkCameraPermission,
              child: const Text("请求相机权限"),
            ),
          ],
        ),
      ),
    );
  }
}

解释

  1. 调用权限需要走两步:
    • Manifest/Info.plist 配置 = “声明”
      • Android:在 AndroidManifest.xml<uses-permission> 声明
      • iOS:在 Info.plist 里写 NSxxxxUsageDescription 描述(必须写,不然直接崩溃)
      • 没有这一步,系统不会允许你在运行时请求权限,或者直接报错。
    • 代码里调用 Permission.xxx.request() = “申请”
      • 声明了权限以后,你才能在代码里调用 permission_handler 的 API 去请求用户授权
      • 用户可以选择 “允许/拒绝/不再询问”
      • 需要根据返回结果决定接下来的逻辑
  2. 权限已经被授予 时,再次调用Permission.camera.request()系统不会再弹窗。因为 Android/iOS 的逻辑是:允许过一次 → 以后默认就是允许

如何使用

常用权限类型

permission_handler 把不同平台的权限封装成了一个统一的 Permission 枚举,比如:

  • Permission.camera → 摄像头
  • Permission.microphone → 麦克风
  • Permission.location → 位置信息
  • Permission.photos → 相册
  • Permission.storage → 存储
  • Permission.notification → 推送通知
  • Permission.bluetooth → 蓝牙

基本用法

检查权限状态

import 'package:permission_handler/permission_handler.dart';

Future<void> checkPermission() async {
  var status = await Permission.camera.status;

  if (status.isGranted) {
    print("相机权限已获取");
  } else if (status.isDenied) {
    print("相机权限被拒绝,还可以再次请求");
  } else if (status.isPermanentlyDenied) {
    print("相机权限永久拒绝,需要去设置里手动开启");
  }
}

请求权限

Future<void> requestPermission() async {
  var status = await Permission.camera.request();
  if (status.isGranted) {
    print("相机权限申请成功");
  } else {
    print("相机权限被拒绝");
  }
}

一次请求多个权限

Future<void> requestMultiple() async {
  Map<Permission, PermissionStatus> statuses = await [
    Permission.camera,
    Permission.microphone,
  ].request();

  if (statuses[Permission.camera]!.isGranted &&
      statuses[Permission.microphone]!.isGranted) {
    print("相机和麦克风权限都已获取");
  }
}

打开应用设置

如果用户选择了「不再询问」,你可以引导用户去设置里手动开启:

await openAppSettings();

相关配置

IOS配置

ios/Podfile

如果需要相关权限,就把相关的注释打开

# 注释掉原来的 post_install
# post_install do |installer|
#   installer.pods_project.targets.each do |target|
#     flutter_additional_ios_build_settings(target)
#   end
# end

post_install do |installer|
  installer.pods_project.targets.each do |target|
    flutter_additional_ios_build_settings(target)

    target.build_configurations.each do |config|
      # You can remove unused permissions here
      # for more information: https://github.com/BaseflowIT/flutter-permission-handler/blob/master/permission_handler/ios/Classes/PermissionHandlerEnums.h
      # e.g. when you don't need camera permission, just add 'PERMISSION_CAMERA=0'
      config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
        '$(inherited)',

        ## dart: PermissionGroup.calendar
        # 'PERMISSION_EVENTS=1',
        
        ## dart: PermissionGroup.calendarFullAccess
        # 'PERMISSION_EVENTS_FULL_ACCESS=1',

        ## dart: PermissionGroup.reminders
        # 'PERMISSION_REMINDERS=1',

        ## dart: PermissionGroup.contacts
        # 'PERMISSION_CONTACTS=1',

        ## dart: PermissionGroup.camera
        'PERMISSION_CAMERA=1',

        ## dart: PermissionGroup.microphone
        'PERMISSION_MICROPHONE=1',

        ## dart: PermissionGroup.speech
        'PERMISSION_SPEECH_RECOGNIZER=1',

        ## dart: PermissionGroup.photos
        'PERMISSION_PHOTOS=1',

        ## The 'PERMISSION_LOCATION' macro enables the `locationWhenInUse` and `locationAlways` permission. If
        ## the application only requires `locationWhenInUse`, only specify the `PERMISSION_LOCATION_WHENINUSE`
        ## macro.
        ##
        ## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse]
        # 'PERMISSION_LOCATION=1',
        # 'PERMISSION_LOCATION_WHENINUSE=0',

        ## dart: PermissionGroup.notification
        # 'PERMISSION_NOTIFICATIONS=1',

        ## dart: PermissionGroup.mediaLibrary
        'PERMISSION_MEDIA_LIBRARY=1',

        ## dart: PermissionGroup.sensors
        # 'PERMISSION_SENSORS=1',

        ## dart: PermissionGroup.bluetooth
        # 'PERMISSION_BLUETOOTH=1',

        ## dart: PermissionGroup.appTrackingTransparency
        # 'PERMISSION_APP_TRACKING_TRANSPARENCY=1',

        ## dart: PermissionGroup.criticalAlerts
        # 'PERMISSION_CRITICAL_ALERTS=1',

        ## dart: PermissionGroup.criticalAlerts
        # 'PERMISSION_ASSISTANT=1',
      ]
    end
  end
end

ios/Runner/Info.plist

<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>

    <!-- 应用名称 -->
    <key>CFBundleName</key>
    <string>MyApp</string>

    <!-- 相机权限 -->
    <key>NSCameraUsageDescription</key>
    <string>需要访问相机拍照或录制视频</string>

    <!-- 麦克风权限 -->
    <key>NSMicrophoneUsageDescription</key>
    <string>需要访问麦克风进行录音或语音识别</string>

    <!-- 照片(相册)读权限 -->
    <key>NSPhotoLibraryUsageDescription</key>
    <string>需要访问照片库以选择或读取图片</string>

    <!-- 照片写权限(iOS 11+,允许保存图片到相册) -->
    <key>NSPhotoLibraryAddUsageDescription</key>
    <string>需要访问照片库以保存图片或视频</string>

    <!-- 媒体资料库(Media Library)权限 -->
    <key>NSAppleMusicUsageDescription</key>
    <string>需要访问媒体资料库以播放音乐或获取音乐信息</string>

    <!-- 语音识别权限 -->
    <key>NSSpeechRecognitionUsageDescription</key>
    <string>需要语音识别权限以识别用户语音输入</string>

    <!-- 定位权限(前台) -->
    <key>NSLocationWhenInUseUsageDescription</key>
    <string>需要定位权限以获取当前位置</string>

    <!-- 定位权限(后台) -->
    <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
    <string>需要后台定位权限以在后台获取位置</string>

    <!-- 通知权限 -->
    <key>NSUserNotificationUsageDescription</key>
    <string>需要发送通知以提醒用户重要事件</string>

    <!-- 日历权限 -->
    <key>NSCalendarsUsageDescription</key>
    <string>需要访问日历以添加或读取事件</string>

    <!-- 通讯录权限 -->
    <key>NSContactsUsageDescription</key>
    <string>需要访问联系人以选择或读取联系人信息</string>

    <!-- 蓝牙权限(iOS 13+) -->
    <key>NSBluetoothAlwaysUsageDescription</key>
    <string>需要访问蓝牙以连接外部设备</string>

    <!-- 健康权限(HealthKit) -->
    <key>NSHealthShareUsageDescription</key>
    <string>需要访问健康数据以读取用户健康信息</string>
    <key>NSHealthUpdateUsageDescription</key>
    <string>需要访问健康数据以写入用户健康信息</string>

    <!-- 相机和麦克风组合使用(视频通话等) -->
    <!-- 有些插件会同时请求这两个权限,分别配置即可 -->

</dict>
</plist>
key 和 string 的作用
<plist version="1.0">
  <dict>
    <key>NSCameraUsageDescription</key>
    <string>需要访问相机拍照或录制视频</string>
  </dict>
</plist>
  • <key>系统识别的属性名称,告诉 iOS 这条配置对应什么功能或信息。
  • <string><key> 对应的值,系统会把这个字符串显示给用户
    • 系统在弹出权限请求对话框时,会显示 需要访问相机拍照或录制视频 这句话,让用户知道为什么 App 要使用相机。
string 不能不写

Info.plist 中:

<string></string>
<!-- 或者 -->
<string/>
  • 技术上是 合法的 XML,表示字符串为空。

  • 但是,iOS 系统在读取权限用途时,如果 <string> 为空:大多数权限请求会直接被系统拒绝permission_handler 在 Flutter 里会返回 permanentlyDenied。因为 iOS 审核要求权限用途说明 必须写清楚

  • 为什么不能只写一个空 <string>?只是由iOS 的设计原则决定的:

    1. 每个敏感权限必须有用途说明,否则系统认为 App 不合法。
  1. 如果 <string> 为空,系统无法显示给用户,直接拒绝权限请求

安卓配置

android/app/src/main/AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myapp">

    <!-- 相机权限 -->
    <uses-permission android:name="android.permission.CAMERA"/>
    
    <!-- 麦克风权限 -->
    <uses-permission android:name="android.permission.RECORD_AUDIO"/>
    
    <!-- 读取外部存储权限(Android 10 以下) -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    
    <!-- 写入外部存储权限(Android 10 以下) -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    
    <!-- Android 10+ 推荐使用分区存储 (scoped storage) -->
    <!-- 仅在旧版本设备或需要兼容时使用 READ/WRITE_EXTERNAL_STORAGE -->
    
    <!-- 位置权限(前台) -->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
    
    <!-- 位置权限(后台) -->
    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
    
    <!-- 联系人权限 -->
    <uses-permission android:name="android.permission.READ_CONTACTS"/>
    <uses-permission android:name="android.permission.WRITE_CONTACTS"/>
    
    <!-- 日历权限 -->
    <uses-permission android:name="android.permission.READ_CALENDAR"/>
    <uses-permission android:name="android.permission.WRITE_CALENDAR"/>
    
    <!-- 电话权限 -->
    <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
    <uses-permission android:name="android.permission.CALL_PHONE"/>
    
    <!-- 发送/接收短信权限 -->
    <uses-permission android:name="android.permission.SEND_SMS"/>
    <uses-permission android:name="android.permission.RECEIVE_SMS"/>
    <uses-permission android:name="android.permission.READ_SMS"/>
    
    <!-- 传感器权限 -->
    <uses-permission android:name="android.permission.BODY_SENSORS"/>
    
    <!-- 网络状态权限 -->
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.INTERNET"/>
    
    <!-- 蓝牙权限 -->
    <uses-permission android:name="android.permission.BLUETOOTH"/>
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
    
    <!-- 存储访问框架(Scoped Storage)示例 -->
    <!-- Android 11+ 不再需要 WRITE_EXTERNAL_STORAGE,使用 MANAGE_EXTERNAL_STORAGE -->
    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>

</manifest>

注意事项

  1. 声明权限只是第一步
    • Android 6.0+(API 23)之后,需要在运行时请求敏感权限(如相机、位置、麦克风)。
    • Flutter 中使用 permission_handler 或者 Permission.camera.request() 来请求。
  2. 不要声明不需要的权限
    • Google Play 审核会严格检查权限用途,不必要的权限可能导致审核拒绝。
  3. 分区存储 (Scoped Storage)
    • Android 10+ 推荐使用分区存储访问外部文件,尽量避免使用 WRITE_EXTERNAL_STORAGE
  4. 蓝牙与后台定位
    • Android 12+ 蓝牙需要 BLUETOOTH_CONNECTBLUETOOTH_SCAN
    • 后台定位需要特别申请 ACCESS_BACKGROUND_LOCATION 并说明用途。

IOS的多语言设置

问题引入

在上面那个案例中,我们在IOS上面做了如下配置:

<key>NSCameraUsageDescription</key>
<string>需要相机权限来拍照</string>

你会发现,弹窗出现了如下小字:需要相机权限来拍照。可是系统设置的是英文系统。这就产生了矛盾。因此需要设置多语言。

配置文件

配置文件的目录结构

ios/Runner/
├── Info.plist                    # 主配置文件(默认语言)
├── en.lproj/                     # 英文语言目录
│   └── InfoPlist.strings         # 英文翻译(正确命名)
└── zh.lproj/                     # 中文语言目录  
    └── InfoPlist.strings         # 中文翻译(正确命名)
  • Flutter iOS 项目默认没有多语言文件,需要手动创建 *.lproj 目录和 InfoPlist.strings 文件。
  • .lproj 是 iOS 本地化目录的标准后缀。

主配置文件:Info.plist

<key>NSPhotoLibraryUsageDescription</key>
<string>需要访问照片权限以选择图片</string>

英文翻译:InfoPlist.strings

/* Permission usage description - Photo Library */
NSPhotoLibraryUsageDescription = "Access to photo library is required to select images";

中文翻译:InfoPlist.strings

/* 权限用途说明 - 访问照片库 */
NSPhotoLibraryUsageDescription = "需要访问照片权限以选择图片";

把上面的文件加入 Runner target

  1. 虽然文件在目录里,但 Xcode 不见得自动把它们添加为资源。所以需要手动加入

  2. 在 Flutter 项目根目录下运行:open ios/Runner.xcworkspace。这样XCode会自动打开IOS的项目。

  3. 在 Project Navigator 左侧,展开 Runner,找到 en.lproj/InfoPlist.stringszh.lproj/InfoPlist.strings。如果没有,就说明没有加入到Runner里面

  4. 点击鼠标右键,点击Add Files to "Runner"...

  5. 成功加入

代码

import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Permission Demo',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const PermissionExample(),
    );
  }
}

class PermissionExample extends StatefulWidget {
  const PermissionExample({super.key});

  @override
  State<PermissionExample> createState() => _PermissionExampleState();
}

class _PermissionExampleState extends State<PermissionExample> {
  String _status = "未知";

  Future<void> _checkPhotosPermission() async {
    PermissionStatus status = await Permission.photos.request();

    setState(() {
      if (status.isGranted) {
        _status = "照片权限已获取 ✅";
      } else if (status.isDenied) {
        _status = "照片权限被拒绝 ❌";
      } else if (status.isPermanentlyDenied) {
        _status = "照片权限永久拒绝,需要去设置开启 ⚠️";
      } else {
        _status = "照片权限状态: $status";
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("Permission Demo")),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(_status, style: const TextStyle(fontSize: 18)),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: _checkPhotosPermission,
              child: const Text("请求照片权限"),
            ),
          ],
        ),
      ),
    );
  }
}

×

喜欢就点赞,疼爱就打赏