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("请求相机权限"),
),
],
),
),
);
}
}
解释
- 调用权限需要走两步:
- Manifest/Info.plist 配置 = “声明”
- Android:在
AndroidManifest.xml里<uses-permission>声明 - iOS:在
Info.plist里写NSxxxxUsageDescription描述(必须写,不然直接崩溃) - 没有这一步,系统不会允许你在运行时请求权限,或者直接报错。
- Android:在
- 代码里调用
Permission.xxx.request()= “申请”- 声明了权限以后,你才能在代码里调用
permission_handler的 API 去请求用户授权 - 用户可以选择 “允许/拒绝/不再询问”
- 需要根据返回结果决定接下来的逻辑
- 声明了权限以后,你才能在代码里调用
- Manifest/Info.plist 配置 = “声明”
- 当 权限已经被授予 时,再次调用
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 的设计原则决定的:- 每个敏感权限必须有用途说明,否则系统认为 App 不合法。
- 如果
<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>
注意事项
- 声明权限只是第一步
- Android 6.0+(API 23)之后,需要在运行时请求敏感权限(如相机、位置、麦克风)。
- Flutter 中使用
permission_handler或者Permission.camera.request()来请求。
- 不要声明不需要的权限
- Google Play 审核会严格检查权限用途,不必要的权限可能导致审核拒绝。
- 分区存储 (Scoped Storage)
- Android 10+ 推荐使用分区存储访问外部文件,尽量避免使用
WRITE_EXTERNAL_STORAGE。
- Android 10+ 推荐使用分区存储访问外部文件,尽量避免使用
- 蓝牙与后台定位
- Android 12+ 蓝牙需要
BLUETOOTH_CONNECT和BLUETOOTH_SCAN。 - 后台定位需要特别申请
ACCESS_BACKGROUND_LOCATION并说明用途。
- Android 12+ 蓝牙需要
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
虽然文件在目录里,但 Xcode 不见得自动把它们添加为资源。所以需要手动加入
在 Flutter 项目根目录下运行:
open ios/Runner.xcworkspace。这样XCode会自动打开IOS的项目。
在 Project Navigator 左侧,展开 Runner,找到
en.lproj/InfoPlist.strings和zh.lproj/InfoPlist.strings。如果没有,就说明没有加入到Runner里面点击鼠标右键,点击
Add Files to "Runner"...
成功加入

代码
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("请求照片权限"),
),
],
),
),
);
}
}