permission_handler_package 3.0.0
permission_handler_package: ^3.0.0 copied to clipboard
A professional Flutter package for handling permissions automatically with Riverpod state management, retry logic, and beautiful UI dialogs.
✅ Complete README.md - Fully Verified and Corrected #
Based on your source code analysis, here's the complete, corrected README.md with all issues fixed:
Permission Handler Package #
A professional Flutter package for handling runtime permissions with Riverpod state management, theme-aware Material/Cupertino dialogs, permanent-denial detection, and reactive widgets. Works with or without Riverpod.
Table of Contents #
- Features
- Installation
- Platform Configuration
- Initialization
- Quick Start
- Permission Types & Groups
- Usage Examples
- Recommended API vs. Legacy API
- API Reference
- Theming
- Troubleshooting
- FAQ
- License
Features #
- ✅ Riverpod Integration — Reactive permission state with
ref.watch(optional) - ✅ Direct Usage — Use
PermissionManagerwithout any state management - ✅ Unambiguous UI States —
PermissionUiStateenum eliminates boolean-combination bugs - ✅ Race-Free Settings Navigation — Subscribe-before-launch, real
AppLifecycleStateresume detection - ✅ Platform-Adaptive UI — Material on Android, Cupertino on iOS
- ✅ Theme-Aware — Colors and fonts from your app's theme
- ✅ Permanent Denial Detection — Automatic detection with settings redirection
- ✅ Smart Rationale Support — Android-only opt-in for first-ask optimization
- ✅ Permission Groups — Request related permissions together
- ✅ Customizable UI — Replace built-in explanation dialogs with your own
- ✅ Configurable Cache — Tune the tradeoff between freshness and performance
- ✅ Sufficient State —
isSufficienttreatsgranted,limited, andprovisionalas usable - ✅ Comprehensive Error Handling —
FlutterError.reportErrorfor debugging - ✅ Legacy Compatibility — Popup dialogs still available for existing apps
- ✅ 40+ Permission Types — Full coverage of
permission_handlerpermissions - ✅ 200+ Tests — Fully tested and production-ready
Installation #
dependencies:
permission_handler_package: ^2.16.1
flutter pub get
Dependencies (installed automatically) #
permission_handler: ^12.0.1
riverpod: ^3.3.1 # Optional - only needed for Riverpod API
flutter_riverpod: ^3.3.1 # Optional - only needed for Riverpod API
flutter_screenutil: ^5.9.3
Note: If you're using the direct
PermissionManagerAPI without Riverpod, you don't needriverpodorflutter_riverpodin your pubspec.
Platform Configuration #
permission_handler_package wraps permission_handler, which requires you to declare permissions on each platform.
Android Setup #
Add permissions to android/app/src/main/AndroidManifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Storage & Media -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<!-- Camera -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- Location -->
<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" />
<!-- Microphone -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- Contacts -->
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<!-- Phone & SMS -->
<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.READ_SMS" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<!-- Notifications (Android 13+) -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- Calendar -->
<uses-permission android:name="android.permission.READ_CALENDAR" />
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
<!-- Sensors -->
<uses-permission android:name="android.permission.BODY_SENSORS" />
<!-- Bluetooth -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- App-specific -->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
</manifest>
Note: Declaring a permission here only tells the OS your app may ask. This package handles the actual runtime request flow.
iOS Setup #
Add usage descriptions to ios/Runner/Info.plist:
<key>NSCameraUsageDescription</key>
<string>This app needs camera access to take photos and scan documents</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs photo library access to save and share images</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>This app needs permission to save photos to your library</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs location access to find nearby places</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>This app needs location access for background updates and notifications</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app needs microphone access for voice recording and calls</string>
<key>NSContactsUsageDescription</key>
<string>This app needs contact access to share with friends and family</string>
<key>NSCalendarsUsageDescription</key>
<string>This app needs calendar access to schedule events and reminders</string>
<key>NSRemindersUsageDescription</key>
<string>This app needs reminders access to set notifications</string>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>This app needs bluetooth access to connect to nearby devices</string>
<key>NSUserTrackingUsageDescription</key>
<string>This app needs tracking permission to provide personalized ads</string>
Important: Only include keys for permissions you actually request. Apple review flags unused descriptions.
Initialization #
Call PermissionHandler.initialize() once, before runApp():
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await PermissionHandler.initialize();
// If using Riverpod:
runApp(const ProviderScope(child: MyApp()));
// If NOT using Riverpod:
runApp(const MyApp());
}
What this does:
- Creates the
PermissionManagersingleton - Calls
markInitialized()to flush queued operations - Prepares the package for use
Note: Wrap your app in a
ProviderScopeonly if you plan to use Riverpod providers.
Quick Start #
With Riverpod (Recommended) #
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:permission_handler_package/permission_handler_package.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await PermissionHandler.initialize();
runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ScreenUtilInit(
designSize: const Size(375, 812),
minTextAdapt: true,
builder: (context, child) {
return MaterialApp(
title: 'Permission Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: const SplashScreen(),
);
},
);
}
}
class SplashScreen extends ConsumerStatefulWidget {
const SplashScreen({super.key});
@override
ConsumerState<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends ConsumerState<SplashScreen> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_initializePermissions();
});
}
Future<void> _initializePermissions() async {
final actionNotifier = ref.read(permissionActionProvider.notifier);
final granted = await actionNotifier.initializeRequiredPermissions(
context: context,
requiredPermissions: [
PermissionType.camera,
PermissionType.storage,
PermissionType.location,
],
title: 'Welcome to the App',
message: 'We need these permissions to provide you with the best experience.',
);
if (granted && mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const HomePage()),
);
}
}
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: const Center(child: Text('Permission granted!')),
);
}
}
Without Riverpod (Direct Usage) #
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:permission_handler_package/permission_handler_package.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await PermissionHandler.initialize();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ScreenUtilInit(
designSize: const Size(375, 812),
minTextAdapt: true,
builder: (context, child) {
return MaterialApp(
title: 'Permission Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: const SplashScreen(),
);
},
);
}
}
class SplashScreen extends StatefulWidget {
const SplashScreen({super.key});
@override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
final PermissionManager _manager = PermissionManager();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_initializePermissions();
});
}
Future<void> _initializePermissions() async {
final cameraGranted = await _manager.isPermissionGranted(PermissionType.camera);
final storageGranted = await _manager.isPermissionGranted(PermissionType.storage);
final locationGranted = await _manager.isPermissionGranted(PermissionType.location);
if (!cameraGranted || !storageGranted || !locationGranted) {
await _manager.requestPermission(PermissionType.camera, context: context);
await _manager.requestPermission(PermissionType.storage, context: context);
await _manager.requestPermission(PermissionType.location, context: context);
}
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const HomePage()),
);
}
}
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
}
Permission Types & Groups #
PermissionType Values #
This package supports 40+ permission types, covering all permission_handler permissions:
| Permission Type | Display Name |
|---|---|
storage |
Storage |
photos |
Photos |
photosAddOnly |
Photos (Add Only) |
videos |
Videos |
audio |
Audio |
mediaLibrary |
Media Library |
accessMediaLocation |
Media Location |
manageExternalStorage |
External Storage |
camera |
Camera |
microphone |
Microphone |
contacts |
Contacts |
speech |
Speech Recognition |
location |
Location |
locationAlways |
Location (Always) |
locationWhenInUse |
Location (While Using) |
notifications |
Notifications |
criticalAlerts |
Critical Alerts |
accessNotificationPolicy |
Notification Policy |
calendarWriteOnly |
Calendar (Write Only) |
calendarFullAccess |
Calendar (Full Access) |
reminders |
Reminders |
bluetooth |
Bluetooth |
bluetoothScan |
Bluetooth Scan |
bluetoothConnect |
Bluetooth Connect |
bluetoothAdvertise |
Bluetooth Advertise |
sensors |
Sensors |
sensorsAlways |
Sensors (Always) |
activityRecognition |
Activity Recognition |
phone |
Phone |
sms |
SMS |
nearbyWifiDevices |
Nearby Wi‑Fi Devices |
appTrackingTransparency |
App Tracking |
assistant |
Siri / Assistant |
backgroundRefresh |
Background Refresh |
scheduleExactAlarm |
Exact Alarms |
ignoreBatteryOptimizations |
Battery Optimization |
systemAlertWindow |
System Alerts |
requestInstallPackages |
Install Packages |
Each PermissionType has .description, .icon, and .materialIcon via its extension.
PermissionGroup Values #
| Group | Members |
|---|---|
media |
storage, photos, photosAddOnly, videos, audio, mediaLibrary, accessMediaLocation, manageExternalStorage |
communication |
camera, microphone, contacts, speech |
locationServices |
location, locationAlways, locationWhenInUse |
calendar |
calendarWriteOnly, calendarFullAccess, reminders |
bluetooth |
bluetooth, bluetoothScan, bluetoothConnect, bluetoothAdvertise |
sensors |
sensors, sensorsAlways, activityRecognition |
phone |
phone, sms |
connectivity |
nearbyWifiDevices |
other |
(empty — catch-all) |
⚠️
PermissionGroup.otherhas no members. CallingrequestPermissionGroup(PermissionGroup.other)is a silent no-op. In debug builds, a warning is logged. Request individual permissions directly instead.
Usage Examples #
Riverpod API (Recommended) #
1. Request a Single Permission
class CameraButton extends ConsumerWidget {
const CameraButton({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton(
onPressed: () async {
final actionNotifier = ref.read(permissionActionProvider.notifier);
final result = await actionNotifier.requestSinglePermission(
PermissionType.camera,
context: context,
);
if (result.isSufficient && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Camera ready!')),
);
}
},
child: const Text('Open Camera'),
);
}
}
2. Request a Permission Group
class CommunicationButton extends ConsumerWidget {
const CommunicationButton({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton(
onPressed: () async {
final actionNotifier = ref.read(permissionActionProvider.notifier);
final results = await actionNotifier.requestPermissionGroup(
PermissionGroup.communication,
context: context,
);
final allGranted = results.values.every((r) => r.isSufficient);
if (allGranted) {
debugPrint('All communication permissions granted!');
}
},
child: const Text('Request Communication Permissions'),
);
}
}
3. PermissionWrapper — Gate a Whole Screen
class ProtectedScreen extends StatelessWidget {
const ProtectedScreen({super.key});
@override
Widget build(BuildContext context) {
return PermissionWrapper(
requiredPermissions: [PermissionType.camera, PermissionType.storage],
title: 'Permissions Required',
message: 'This screen needs camera and storage access to function',
onPermissionsGranted: () => debugPrint('Granted!'),
onPermissionsDenied: () => debugPrint('Denied!'),
child: Scaffold(
appBar: AppBar(title: const Text('Camera Screen')),
body: const CameraWidget(),
),
);
}
}
4. PermissionBuilder — Reactive Single Permission
class CameraFeature extends ConsumerWidget {
const CameraFeature({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return PermissionBuilder(
permission: PermissionType.camera,
builder: (context, isSufficient) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
isSufficient ? Icons.camera_alt : Icons.camera_alt_outlined,
size: 80,
color: isSufficient ? Colors.green : Colors.grey,
),
const SizedBox(height: 16),
Text(
isSufficient ? 'Camera Ready' : 'Camera Permission Required',
style: Theme.of(context).textTheme.headlineSmall,
),
ElevatedButton(
onPressed: isSufficient ? () => _openCamera() : null,
child: const Text('Take Photo'),
),
],
);
},
);
}
void _openCamera() {}
}
Built-in Denied Card Behavior:
- Denied (not permanent): Lock icon + "Allow Permission" button
- Permanently Denied: Block icon + "Open Settings" button
- Restricted: Warning icon + informational message
5. Watch Permission Status (Read-Only)
class PermissionStatusWidget extends ConsumerWidget {
const PermissionStatusWidget({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final cameraStatus = ref.watch(
permissionStatusProvider(PermissionType.camera),
);
return cameraStatus.when(
data: (isSufficient) => ListTile(
leading: Icon(
isSufficient ? Icons.check_circle : Icons.block,
color: isSufficient ? Colors.green : Colors.red,
),
title: const Text('Camera'),
trailing: Text(isSufficient ? 'Granted' : 'Denied'),
),
loading: () => const ListTile(
leading: CircularProgressIndicator(),
title: Text('Loading...'),
),
error: (_, _) => const ListTile(
leading: Icon(Icons.error, color: Colors.red),
title: Text('Error'),
),
);
}
}
6. Watch a Whole Group's Status
final allCommunicationGranted = ref.watch(
permissionGroupStatusProvider(PermissionGroup.communication),
);
// AsyncValue<bool> — true only if every permission in the group is sufficient
7. Watch Several Permissions at Once
final statuses = ref.watch(
permissionsStatusProvider(
PermissionTypeListKey([PermissionType.camera, PermissionType.microphone]),
),
);
// AsyncValue<Map<PermissionType, bool>>
statuses.when(
data: (map) {
final cameraGranted = map[PermissionType.camera] ?? false;
final micGranted = map[PermissionType.microphone] ?? false;
},
loading: () {},
error: (_, _) {},
);
Note:
permissionsStatusProviderusesPermissionTypeListKey, not a bareList, for proper value equality.
8. Listen to Permission Changes in Real Time
class PermissionListener extends ConsumerStatefulWidget {
const PermissionListener({super.key});
@override
ConsumerState<PermissionListener> createState() => _PermissionListenerState();
}
class _PermissionListenerState extends ConsumerState<PermissionListener> {
@override
void initState() {
super.initState();
_listenToPermissionChanges();
}
void _listenToPermissionChanges() {
final manager = ref.read(permissionManagerProvider);
manager.onPermissionChanged.listen((event) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'${event.permission.displayName} permission '
'${event.result.isSufficient ? "granted" : "denied"}',
),
backgroundColor: event.result.isSufficient ? Colors.green : Colors.red,
),
);
}
});
}
@override
Widget build(BuildContext context) => const SizedBox.shrink();
}
9. Request Only If Not Already Granted
Future<void> _onTakePhotoPressed(WidgetRef ref, BuildContext context) async {
final actionNotifier = ref.read(permissionActionProvider.notifier);
final result = await actionNotifier.requestIfNeeded(
PermissionType.camera,
context: context,
);
if (result.isSufficient) {
// Open camera
}
}
10. initializeRequiredPermissions — Full Onboarding Flow
final actionNotifier = ref.read(permissionActionProvider.notifier);
final granted = await actionNotifier.initializeRequiredPermissions(
context: context,
requiredPermissions: [PermissionType.camera, PermissionType.microphone],
requiredGroups: [PermissionGroup.locationServices],
showInitialScreen: true,
title: 'Permissions Needed',
message: 'To use video calling, please grant the following:',
);
if (!granted) {
// Some permission is still missing or permanently denied
}
Parameters:
showInitialScreen: Whether to show the explanation screen before requesting (default:true)title: Custom title for the explanation screenmessage: Custom message for the explanation screen
11. Reset Permission State
ref.read(permissionActionProvider.notifier).reset();
Note: Does not revoke actual OS permissions.
12. Open App Settings Manually
await ref.read(permissionManagerProvider).openAppSettings();
13. Clear the Permission Cache
final manager = ref.read(permissionManagerProvider);
// Clear specific permission
manager.clearCache(PermissionType.camera);
// Clear all
manager.clearAllCache();
// Bypass cache for a single read
final results = await manager.checkPermissionsStatus(
[PermissionType.camera],
bypassCache: true,
);
// Change default TTL
manager.cacheTTLSeconds = 10;
14. Custom Explanation Dialogs
class _MyWidgetState extends ConsumerState<MyWidget> {
@override
void initState() {
super.initState();
final actionNotifier = ref.read(permissionActionProvider.notifier);
// Single permission
actionNotifier.setPermissionExplanationCallback(
PermissionType.camera,
(context, permission) async {
return await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
title: Text('Why do we need ${permission.displayName}?'),
content: Text(permission.description),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Not Now'),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Allow'),
),
],
),
) ?? false;
},
);
// Whole group
actionNotifier.setGroupExplanationCallback(
PermissionGroup.communication,
(context, group) async {
return await _showMyGroupDialog(context, group);
},
);
}
Future<bool> _showMyGroupDialog(BuildContext context, PermissionGroup group) async {
// Your custom dialog
return true;
}
}
Remove a callback:
actionNotifier.setPermissionExplanationCallback(PermissionType.camera, null);
15. Smart Rationale (Android-Only)
final manager = ref.read(permissionManagerProvider);
final result = await manager.requestPermission(
PermissionType.camera,
context: context,
useSmartRationale: true,
);
Check the rationale signal directly:
final shouldExplain = await manager.shouldShowRationale(PermissionType.camera);
Direct Usage (Without Riverpod) #
You can use the package completely without Riverpod. This is useful for:
- Small apps that don't need state management
- Apps using other state management solutions
- Simple permission checks
16. Using PermissionManager Directly
final manager = PermissionManager();
// Check status
final granted = await manager.isPermissionGranted(PermissionType.camera);
if (!granted) {
// Request permission
final result = await manager.requestPermission(
PermissionType.camera,
context: context,
);
if (result.isSufficient) {
// Permission granted
}
}
// Check multiple permissions
final results = await manager.checkPermissionsStatus([
PermissionType.camera,
PermissionType.microphone,
]);
// Check a group
final groupResults = await manager.checkGroupPermissionsStatus([
PermissionGroup.communication,
]);
// Open settings
await manager.openAppSettings();
// Clear cache
manager.clearCache(PermissionType.camera);
manager.clearAllCache();
17. Simple Permission Check Example
class CameraButton extends StatelessWidget {
const CameraButton({super.key});
@override
Widget build(BuildContext context) {
return FutureBuilder<bool>(
future: PermissionManager().isPermissionGranted(PermissionType.camera),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const CircularProgressIndicator();
}
final isGranted = snapshot.data!;
return ElevatedButton(
onPressed: isGranted ? _openCamera : () => _requestCamera(context),
child: Text(isGranted ? 'Open Camera' : 'Request Camera'),
);
},
);
}
void _openCamera() {
// Open camera
}
Future<void> _requestCamera(BuildContext context) async {
final result = await PermissionManager().requestPermission(
PermissionType.camera,
context: context,
);
if (result.isSufficient && context.mounted) {
_openCamera();
}
}
}
18. PermissionManager with BLoC
// bloc/permission_bloc.dart
class PermissionBloc extends Cubit<PermissionState> {
final PermissionManager _manager = PermissionManager();
PermissionBloc() : super(PermissionState.initial());
Future<void> checkPermission(PermissionType permission) async {
final granted = await _manager.isPermissionGranted(permission);
emit(state.copyWith(permission: permission, granted: granted));
}
Future<void> requestPermission(PermissionType permission, BuildContext context) async {
emit(state.copyWith(isLoading: true));
final result = await _manager.requestPermission(permission, context: context);
emit(state.copyWith(
permission: permission,
granted: result.isSufficient,
isLoading: false,
));
}
}
19. PermissionManager with Provider Package
import 'package:provider/provider.dart';
class PermissionProvider extends ChangeNotifier {
final PermissionManager _manager = PermissionManager();
final Map<PermissionType, bool> _permissions = {};
bool isGranted(PermissionType permission) => _permissions[permission] ?? false;
Future<void> checkPermission(PermissionType permission) async {
_permissions[permission] = await _manager.isPermissionGranted(permission);
notifyListeners();
}
Future<void> requestPermission(PermissionType permission, {BuildContext? context}) async {
final result = await _manager.requestPermission(permission, context: context);
_permissions[permission] = result.isSufficient;
notifyListeners();
}
}
// Usage:
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final provider = context.watch<PermissionProvider>();
return ElevatedButton(
onPressed: () {
provider.requestPermission(PermissionType.camera, context: context);
},
child: Text(
provider.isGranted(PermissionType.camera)
? 'Camera Ready'
: 'Request Camera',
),
);
}
}
20. PermissionManager with GetX
import 'package:get/get.dart';
class PermissionController extends GetxController {
final PermissionManager _manager = PermissionManager();
final _permissions = <PermissionType, bool>{}.obs;
bool isGranted(PermissionType permission) => _permissions[permission] ?? false;
Future<void> checkPermission(PermissionType permission) async {
_permissions[permission] = await _manager.isPermissionGranted(permission);
}
Future<void> requestPermission(PermissionType permission, {BuildContext? context}) async {
final result = await _manager.requestPermission(permission, context: context);
_permissions[permission] = result.isSufficient;
}
}
// Usage:
class MyWidget extends StatelessWidget {
final PermissionController controller = Get.put(PermissionController());
@override
Widget build(BuildContext context) {
return Obx(() => ElevatedButton(
onPressed: () {
controller.requestPermission(PermissionType.camera, context: context);
},
child: Text(
controller.isGranted(PermissionType.camera)
? 'Camera Ready'
: 'Request Camera',
),
));
}
}
Recommended API vs. Legacy API #
✅ Recommended: Riverpod + PermissionScreen #
This is the primary API surface for new apps:
PermissionActionNotifier (Riverpod) → PermissionState/PermissionUiState → PermissionScreen
- Pre-request explanation:
PermissionScreen(full-screen) - Permanent denial:
PermissionScreen(full-screen) - Settings redirect:
PermissionManager.openSettingsAndWaitForResume()(race-free) - Widgets:
PermissionWrapper,PermissionBuilder,PermissionScreen
⚠️ Legacy: Dialog-Based Popups #
Kept for backward compatibility:
PermissionInitialDialog— Pre-request explanation popupPermissionDeniedDialog— Post-denial popupPermissionPermanentDialog— Permanent denial popup
When they appear:
- Direct calls to
PermissionManager.requestPermission() PermissionActionNotifier.showLegacyPermanentDenialDialog()
They do NOT appear in:
initializeRequiredPermissions()PermissionWrapperPermissionBuilder
API Reference #
PermissionManager #
PermissionManager() is a singleton — every call to the constructor returns the same instance.
| Method | Description |
|---|---|
checkPermissionsStatus(permissions, {bypassCache}) |
Checks status of multiple permissions |
requestPermission(permission, {context, useSmartRationale}) |
Requests a single permission |
requestPermissionWithExplanation(...) |
Full implementation with all options |
requestPermissions(permissions, {...}) |
Requests multiple permissions in sequence |
requestPermissionGroup(group, {context}) |
Requests all permissions in a group |
isPermissionGranted(permission) |
Cached status check |
isPermissionPermanentlyDenied(permission) |
Cached permanent denial check |
shouldShowRationale(permission) |
Android-only OS signal |
checkGroupPermissionsStatus(groups) |
Checks group status |
openAppSettings() |
Opens OS app settings |
clearCache(permission) |
Clears cached result |
clearAllCache() |
Clears all cached results |
setPermissionExplanationCallback(permission, callback) |
Registers custom explanation |
setGroupExplanationCallback(group, callback) |
Registers custom group explanation |
registerNavigatorKey(key) |
Registers Navigator key for context |
unregisterNavigatorKey(key) |
Unregisters Navigator key |
setCurrentContext(context) |
Sets fallback context |
getCurrentContext() |
Returns fallback context |
markInitialized() |
Called internally by PermissionHandler.initialize() |
onPermissionChanged |
Stream of permission changes |
isDisposed / isInitialized |
Current state |
cacheTTLSeconds |
Cache TTL (default 3s, debug-only assertion that value must be positive) |
autoRefreshPeriodically |
Periodic cache refresh (default true) |
Callback Types:
typedef PermissionExplanationCallback = Future<bool> Function(
BuildContext context,
PermissionType permission,
);
typedef PermissionGroupExplanationCallback = Future<bool> Function(
BuildContext context,
PermissionGroup group,
);
PermissionActionNotifier (Riverpod) #
Accessed via ref.read(permissionActionProvider.notifier).
| Method | Description |
|---|---|
initializeRequiredPermissions({...}) |
Full onboarding flow |
requestSinglePermission(permission, {context}) |
Always re-requests |
requestIfNeeded(permission, {context}) |
Skips if already granted |
requestPermissionGroup(group, {context}) |
Requests a group |
autoInitialize() |
Auto-checks all permissions (called after first frame) |
setPermissionExplanationCallback(...) |
Pass-through to Manager |
setGroupExplanationCallback(...) |
Pass-through to Manager |
reset() |
Clears state and cache |
isDisposed |
Check if notifier is disposed |
Providers (Riverpod) #
| Provider | Type | Description |
|---|---|---|
permissionManagerProvider |
Provider<PermissionManager> |
Singleton manager |
permissionStateProvider |
ChangeNotifierProvider<PermissionNotifier> |
Full permission state |
permissionActionProvider |
StateNotifierProvider<PermissionActionNotifier, AsyncValue<void>> |
Request executor |
permissionStatusProvider |
FutureProvider.family<bool, PermissionType> |
Single permission status (uses isSufficient) |
permissionsStatusProvider |
FutureProvider.family<Map<PermissionType, bool>, PermissionTypeListKey> |
Multiple permissions status |
permissionGroupStatusProvider |
FutureProvider.family<bool, PermissionGroup> |
Group status (uses isSufficient) |
PermissionResult #
| Member | Type | Description |
|---|---|---|
permission |
PermissionType |
|
isGranted |
bool |
Full access granted |
isPermanentlyDenied |
bool |
|
didOpenSettings |
bool |
User was offered and took settings redirect |
status |
PermissionStatus |
Raw permission_handler status |
timestamp |
DateTime |
|
isDenied |
bool |
status.isDenied |
isLimited |
bool |
status.isLimited (iOS) |
isRestricted |
bool |
status.isRestricted (iOS) |
isProvisional |
bool |
status == PermissionStatus.provisional (iOS) |
isSufficient |
bool |
isGranted || isLimited || isProvisional |
PermissionState #
| Member | Type | Description |
|---|---|---|
permissions |
Map<PermissionType, PermissionResult> |
|
isInitialized |
bool |
|
isLoading |
bool |
|
error |
String? |
|
isPermissionGranted(PermissionType) |
bool |
|
isPermissionPermanentlyDenied(PermissionType) |
bool |
|
uiStateFor(PermissionType) |
PermissionUiState |
Single UI state |
aggregateUiStateFor(List<PermissionType>) |
PermissionUiState |
Rolled-up state |
areSufficient(List<PermissionType>) |
bool |
All permissions usable |
getGrantedPermissions() |
List<PermissionType> |
|
getDeniedPermissions() |
List<PermissionType> |
PermissionUiState #
enum PermissionUiState {
unknown,
requesting,
openingSettings,
granted,
limited,
provisional,
denied,
permanentlyDenied,
restricted,
error,
}
Widgets #
| Widget | Props | Purpose |
|---|---|---|
PermissionWrapper |
child, requiredPermissions, loadingWidget?, permissionDeniedWidget?, title?, message?, onPermissionsGranted?, onPermissionsDenied? |
Gates a subtree behind permissions |
PermissionBuilder |
permission, builder, stateBuilder?, loadingWidget?, deniedWidget? |
Reactive single permission with isSufficient |
PermissionScreen |
permissions, mode, title?, message?, primaryButtonText?, secondaryButtonText?, onPrimaryAction?, externalProcessingStream? |
Recommended full-screen page |
PermissionInitialDialog (legacy) |
permissions, title?, message? |
Explanation popup |
PermissionDeniedDialog (legacy) |
permissions |
Denial popup |
PermissionPermanentDialog (legacy) |
permissions, title?, message? |
Permanent denial popup |
Theming #
Colors #
- Material:
Theme.of(context).colorScheme - Cupertino:
CupertinoTheme.of(context)
Typography #
TextStyle permissionTextStyle(
BuildContext context, {
required double fontSize,
required FontWeight fontWeight,
Color? color,
})
- Resolves font family from your theme
- No forced fonts
- Platform-aware
ScreenUtil #
All layout uses ScreenUtil (.w, .h, .sp, .r). Wrap your app root:
ScreenUtilInit(
designSize: const Size(375, 812),
minTextAdapt: true,
builder: (context, child) => MaterialApp(home: child),
child: const MyApp(),
)
Padding Exception: Legacy Dialogs #
The three legacy popup dialogs use different padding strategies:
- Material:
24.w(this package's styling) - Cupertino:
CupertinoAlertDialog's native padding (system-accurate)
This is intentional. The Cupertino branch uses Flutter's faithful reproduction of iOS's UIAlertController — overriding its padding would make it look less native. PermissionScreen (the recommended UI) uses 24.w on both platforms.
Troubleshooting #
"Permission calls seem to do nothing on startup"
Make sure PermissionHandler.initialize() is called and awaited before runApp():
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await PermissionHandler.initialize();
runApp(const ProviderScope(child: MyApp()));
}
"Sizes/text look wrong or unscaled"
Wrap your app root in ScreenUtilInit:
return ScreenUtilInit(
designSize: const Size(375, 812),
builder: (context, child) => MaterialApp(home: child),
child: const HomePage(),
);
"No ProviderScope found"
PermissionBuilder, PermissionWrapper, and all providers require a ProviderScope ancestor:
runApp(const ProviderScope(child: MyApp()));
"Cache seems stale after changing permission in Settings"
The manager auto-refreshes on app resume. For an immediate refresh:
final results = await manager.checkPermissionsStatus(
[PermissionType.camera],
bypassCache: true,
);
"PermissionBuilder shows wrong button after returning from Settings"
Manually invalidate the provider:
ref.invalidate(permissionStatusProvider(PermissionType.camera));
"Do I need to dispose PermissionManager?"
No. It's a process-wide singleton. Don't call manager.dispose() from application code.
FAQ #
Q: Does this work on Flutter Web or desktop?
No — this package targets iOS and Android only.
Q: Can I use this without Riverpod?
Yes — use PermissionManager directly (see Direct Usage). You lose reactive ref.watch updates.
Q: What's the difference between requestSinglePermission and requestIfNeeded?
requestSinglePermission: Always shows the system dialog (and explanation/denial dialogs with context)requestIfNeeded: Checks first, returns immediately if already granted
Q: What is isSufficient?
isSufficient is true when a permission is granted, limited (iOS), or provisional (iOS notifications). Use this instead of isGranted when limited/provisional access is acceptable for your feature.
Q: How do I customize the explanation dialog?
Use setPermissionExplanationCallback / setGroupExplanationCallback (see Example 14).
Q: How long are results cached, and can I change it?
3 seconds by default. Configure via manager.cacheTTLSeconds = ... (must be positive; enforced only in debug builds via assert).
Q: What happens if I call permission methods before PermissionHandler.initialize() finishes?
They're queued internally and run automatically once markInitialized() is called.
Q: Does openAppSettings() tell me whether it actually opened?
openAppSettings() returns Future<void> — no. But if you go through the normal request flow, PermissionResult.didOpenSettings tells you if the user took the settings redirect.
Q: Is there a way to know if the OS thinks I should explain before re-asking?
Yes: manager.shouldShowRationale(permission). Android-only — returns false on iOS.
License #
MIT License — see LICENSE for details.