深色模式
Flutter移动端动态权限处理
业务场景与权限处理流程
移动端权限管理的重要性
在移动应用开发中,权限管理是用户体验和应用功能实现的关键环节。随着用户隐私保护意识的增强和各平台合规要求的提升,如何优雅地处理权限请求成为开发者必须面对的挑战。
常见的权限使用场景包括:
- 相机权限:拍照、扫码、视频通话
- 麦克风权限:录音、语音通话、语音识别
- 位置权限:地图导航、附近服务、位置分享
- 存储权限:文件上传下载、图片保存
- 通知权限:消息推送、提醒功能
完整的权限处理流程
权限处理的核心流程是:先检查权限状态,再根据状态采取相应行动。
检查权限的三种状态:
- 已授权:直接使用相关功能
- 需要请求(首次请求或暂时拒绝):显示权限用途说明,然后发起权限请求
- 永久拒绝:引导用户前往系统设置手动开启权限
请求权限的三种结果:
- 用户同意:获得授权,可以使用功能
- 用户拒绝:提示用户功能无法使用
- 永久拒绝:引导用户前往系统设置
Android与iOS平台差异:
- Android:连续拒绝2次后变为永久拒绝(Android 11+)
- iOS:首次拒绝后即为永久拒绝,需要用户主动在设置中开启
技术实现方案
使用的第三方库
本方案使用两个核心库来实现权限管理:
1. permission_handler
- 功能:检查权限状态、发起权限请求
- 地址:https://pub.dev/packages/permission_handler
- 特点:支持多平台,API统一
2. app_settings
- 功能:跳转到系统设置页面
- 地址:https://pub.dev/packages/app_settings
- 特点:简单易用,支持跳转到具体的权限设置页
配置要求
使用 permission_handler 时需要进行平台配置:
- Android:在
android/app/src/main/AndroidManifest.xml中添加相应权限声明 - iOS:需要同时修改
ios/Runner/Info.plist添加权限描述,以及ios/Podfile添加权限模块
封装类设计与使用示例
设计思路
考虑到当前的合规要求,应用通常避免一次性申请多个权限,因此本封装专注于单权限处理
使用示例
基本使用方法
dart
// 创建权限请求配置
final cameraRequest = PermissionRequest(
permission: Permission.camera,
usageTitle: '相机权限',
usageMessage: '我们需要使用您的相机来拍摄照片,这将帮助您记录美好时刻',
usageButtonText: '好的',
settingsTitle: '无法使用相机',
settingsMessage: '请在系统设置中开启相机权限,以便正常使用拍照功能',
);
// 确保获得权限
final granted = await ensurePermission(cameraRequest);
if (granted) {
// 已获得权限,执行相关操作
_openCamera();
} else {
// 未获得权限,显示替代方案或提示
_showAlternativeOptions();
}不同权限类型的配置示例
dart
// 麦克风权限
final microphoneRequest = PermissionRequest(
permission: Permission.microphone,
usageTitle: '麦克风权限',
usageMessage: '需要使用麦克风进行语音录制和通话功能',
usageButtonText: '允许',
settingsTitle: '麦克风权限被拒绝',
settingsMessage: '请在设置中开启麦克风权限以使用语音功能',
);
// 位置权限
final locationRequest = PermissionRequest(
permission: Permission.location,
usageTitle: '位置权限',
usageMessage: '获取您的位置信息以提供附近服务和导航功能',
usageButtonText: '确定',
settingsTitle: '位置权限未开启',
settingsMessage: '请在系统设置中开启位置权限以获得更好的服务体验',
);完整实现代码
以下是权限处理的完整实现代码,包含了权限状态管理、用户交互和系统设置跳转等功能:
dart
import 'package:app_settings/app_settings.dart';
import 'package:flutter/cupertino.dart';
import 'package:gy/router.dart';
import 'package:permission_handler/permission_handler.dart';
/// 权限状态枚举
enum _Status {
/// 已授权
granted,
/// 需要请求权限(首次或暂时拒绝)
needRequest,
/// 永久拒绝(需要引导用户去设置)
deniedPermanently,
}
/// 权限请求配置类
class PermissionRequest {
const PermissionRequest({
required this.permission,
required this.usageTitle,
required this.usageMessage,
required this.usageButtonText,
required this.settingsTitle,
required this.settingsMessage,
});
/// 要请求的权限类型
final Permission permission;
/// 权限用途说明弹窗标题
final String usageTitle;
/// 权限用途说明弹窗内容
final String usageMessage;
/// 权限用途说明弹窗按钮文本
final String usageButtonText;
/// 设置引导弹窗标题
final String settingsTitle;
/// 设置引导弹窗内容
final String settingsMessage;
}
/// 确保获得权限的核心方法
///
/// 流程:
/// - 检查权限状态:
/// - 已授权:直接返回 true
/// - 需要请求(首次或暂时拒绝):显示使用说明弹窗 -> 请求权限 -> 返回结果
/// - 永久拒绝:显示设置引导弹窗,并立即返回 false
///
/// 返回值:
/// - `true` 表示已获得权限,可以使用相关功能
/// - `false` 表示未获得权限,无法使用相关功能
Future<bool> ensurePermission(PermissionRequest request) async {
// 1. 检查当前权限状态
final status = await _checkPermission(request.permission);
switch (status) {
case _Status.granted:
// 已授权,直接返回成功
return true;
case _Status.needRequest:
// 需要请求权限:先显示使用说明,然后请求权限
await _showUsageDescription(request);
final result = await request.permission.request();
return result.isGranted || result.isLimited;
case _Status.deniedPermanently:
// 永久拒绝:引导用户去设置页面
_showSettingsDialog(request);
return false;
}
}
/// 检查权限状态的私有方法
Future<_Status> _checkPermission(Permission permission) async {
var status = await permission.status;
switch (status) {
// 已授权(包括完全授权和有限授权)
case PermissionStatus.granted:
case PermissionStatus.limited: // 仅iOS的Photo Library有这个状态
return _Status.granted;
// 用户拒绝访问,权限需要先被请求(首次未请求也是此状态)
case PermissionStatus.denied:
return _Status.needRequest;
// 永久拒绝(Android 11+:连续拒绝2次;Android 11以下:选择不再询问;iOS:拒绝过)
case PermissionStatus.permanentlyDenied:
return _Status.deniedPermanently;
// 系统限制(如家长控制),视为永久拒绝
case PermissionStatus.restricted: // 仅iOS有这个状态
return _Status.deniedPermanently;
// 临时通知授权(仅iOS通知),视为已授权
case PermissionStatus.provisional: // 仅iOS有这个状态
return _Status.granted;
}
}
/// 显示权限用途说明弹窗(在请求权限之前)
Future<void> _showUsageDescription(PermissionRequest request) async {
return showCupertinoDialog(
context: GRouter.navState.context,
builder: (context) => CupertinoAlertDialog(
title: Text(request.usageTitle),
content: Text(request.usageMessage),
actions: [
CupertinoDialogAction(
onPressed: () => GRouter.navState.pop(),
child: Text(request.usageButtonText),
),
],
),
);
}
/// 显示设置引导弹窗(永久拒绝时)
void _showSettingsDialog(PermissionRequest request) {
final title = request.settingsTitle;
final message = request.settingsMessage;
showCupertinoDialog(
context: GRouter.navState.context,
builder: (context) => CupertinoAlertDialog(
title: Text(title),
content: Text(message),
actions: [
CupertinoDialogAction(
onPressed: () => GRouter.navState.pop(),
child: const Text('取消'),
),
CupertinoDialogAction(
isDefaultAction: true,
onPressed: () {
GRouter.navState.pop();
AppSettings.openAppSettings();
},
child: const Text('去设置'),
),
],
),
);
}