简介
移动应用经常需要访问设备的各种功能(如相机、位置等),这就需要合理处理权限申请和检查。本文介绍 Flutter 中权限处理的方法。
基本配置
添加依赖
dependencies:
permission_handler: ^10.4.3
配置平台权限
Android (AndroidManifest.xml)
<manifest xmlns:android="...">
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
</manifest>
iOS (Info.plist)
<key>NSCameraUsageDescription</key>
<string>需要访问相机以拍摄照片</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>需要访问相册以选择照片</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>需要访问位置信息以提供相关服务</string>
基本使用
检查权限
Future<bool> checkPermission(Permission permission) async {
final status = await permission.status;
return status.isGranted;
}
请求权限
Future<bool> requestPermission(Permission permission) async {
final status = await permission.request();
return status.isGranted;
}
打开设置
void openSettings() async {
await openAppSettings();
}
完整示例
class PermissionManager {
static final PermissionManager _instance = PermissionManager._internal();
factory PermissionManager() => _instance;
PermissionManager._internal();
// 检查多个权限
Future<Map<Permission, bool>> checkPermissions(
List<Permission> permissions,
) async {
Map<Permission, bool> statuses = {};
for (var permission in permissions) {
statuses[permission] = await checkPermission(permission);
}
return statuses;
}
// 请求多个权限
Future<Map<Permission, bool>> requestPermissions(
List<Permission> permissions,
) async {
Map<Permission, bool> statuses = {};
for (var permission in permissions) {
statuses[permission] = await requestPermission(permission);
}
return statuses;
}
// 检查单个权限
Future<bool> checkPermission(Permission permission) async {
final status = await permission.status;
return status.isGranted;
}
// 请求单个权限
Future<bool> requestPermission(Permission permission) async {
final status = await permission.request();
return status.isGranted;
}
// 检查并请求权限
Future<bool> checkAndRequestPermission(Permission permission) async {
if (await checkPermission(permission)) {
return true;
}
return await requestPermission(permission);
}
}
// 使用示例
class PermissionDemo extends StatefulWidget {
@override
_PermissionDemoState createState() => _PermissionDemoState();
}
class _PermissionDemoState extends State<PermissionDemo> {
final permissionManager = PermissionManager();
Map<Permission, bool> _permissionStatuses = {};
@override
void initState() {
super.initState();
_checkPermissions();
}
Future<void> _checkPermissions() async {
final statuses = await permissionManager.checkPermissions([
Permission.camera,
Permission.storage,
Permission.location,
]);
setState(() {
_permissionStatuses = statuses;
});
}
Future<void> _requestPermission(Permission permission) async {
final granted = await permissionManager.requestPermission(permission);
setState(() {
_permissionStatuses[permission] = granted;
});
if (!granted) {
_showPermissionDialog();
}
}
void _showPermissionDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('需要权限'),
content: Text('请在设置中开启相关权限'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('取消'),
),
TextButton(
onPressed: () {
Navigator.pop(context);
openAppSettings();
},
child: Text('去设置'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('权限管理')),
body: ListView(
children: [
_buildPermissionTile(
'相机权限',
Permission.camera,
Icons.camera_alt,
),
_buildPermissionTile(
'存储权限',
Permission.storage,
Icons.storage,
),
_buildPermissionTile(
'位置权限',
Permission.location,
Icons.location_on,
),
],
),
);
}
Widget _buildPermissionTile(
String title,
Permission permission,
IconData icon,
) {
final granted = _permissionStatuses[permission] ?? false;
return ListTile(
leading: Icon(icon),
title: Text(title),
subtitle: Text(granted ? '已授权' : '未授权'),
trailing: Switch(
value: granted,
onChanged: (value) {
if (value) {
_requestPermission(permission);
}
},
),
);
}
}
最佳实践
- 先查
status再request:用户选了”永久拒绝”后,再调request()弹窗不会再出现,等于白调一次;先查isPermanentlyDenied,是就直接弹二次引导去设置页。 - 权限按需分组申请,别在启动页一次性要相机+定位+存储:三个系统弹窗连击,用户第一反应是”这 App 要偷我数据”,拒绝率飙升;用到哪个申请哪个(申请时机的道理见文末实战案例)。
- iOS 的
UsageDescription文案认真写:审核会看,写”需要您的权限”这种会被打回;写清具体用途——“拍摄身份证照片用于实名认证”,一句话讲清”为什么”。
注意事项
- iOS 端
Info.plist里缺了对应的UsageDescriptionkey,申请时直接崩溃(不是返回 denied):上线前逐个权限检查 plist,别等用户 crash 上报才发现。 - Android 13+ 存储权限拆分:
READ_MEDIA_IMAGES/VIDEO/AUDIO替代了READ_EXTERNAL_STORAGE;targetSdk 33+ 还在用旧权限申请,系统直接无视——按官方迁移文档逐个替换。 - 定位权限别上来就要 always:先申请
whenInUse,等用户信任后再引导 always;iOS 上 always 要走两步授权,审核还会问你为什么需要后台定位,答不上来就被拒。 - Android 上用户可以选”仅本次允许”:别把”这次授权了”缓存成永久状态,下次启动重新查
status——不然功能莫名其妙失效,你还以为是代码 bug。
实战案例:权限申请的时机比代码重要
某物流冷链项目 的身份证认证页用 image_picker 拍照(controller 里 final ImagePicker _picker = ImagePicker();),这个场景让我彻底理解了”申请时机”这件事。一开始的写法是进页面就申请相机权限,用户还没搞明白要干嘛,先被系统弹窗吓一跳,拒绝率很高。改成用户点了”拍摄身份证”按钮才申请,拒绝率直接降下来——因为这时候用户有明确预期,“我要拍照,当然要给相机权限”。被拒之后也别干瞪眼,弹个二次引导说明”去设置里打开”,比直接跳设置页的转化好。trade-off 在定位权限上也一样:别在启动页一次性申请相机+定位+存储三个权限,用户会觉得这 App 要窃取我全部数据。原则就一条:权限申请永远跟在用户动作后面,而不是跟在 App 启动后面。permission_handler 的 API 都是现成的,真正的功夫在产品交互上。
总结
合理的权限处理对于应用的用户体验至关重要。通过使用 permission_handler 插件,可以轻松实现权限的申请和管理。注意在实现过程中要考虑用户体验和平台差异。
