Permission Handler Package
A comprehensive Flutter package for handling runtime permissions with Riverpod state management, theme-aware Material/Cupertino dialogs, permanent-denial detection, and reactive widgets.
Version: 2.1.2
📚 Table of Contents
- Overview
- Installation
- Platform Configuration
- Initialization
- Quick Start
- Permission Types & Groups
- Usage Examples
- 1. Request a Single Permission
- 2. Request a Permission Group
- 3. Gate a Whole Screen with PermissionWrapper
- 4. Reactive Single Permission with PermissionBuilder
- 5. Watch Permission Status (Read-Only)
- 6. Watch a Whole Group's Status
- 7. Watch Multiple Permissions at Once
- 8. Listen to Permission Changes in Real-Time
- 9. Request Only If Not Already Granted
- 10. Full Onboarding Flow
- 11. Reset Permission State
- 12. Open App Settings Manually
- 13. Clear the Permission Cache
- 14. Custom Explanation Dialogs
- 15. Smart Rationale (Android)
- 16. Using PermissionManager Without Riverpod
- API Reference
- Theming
- Troubleshooting
- FAQ
- License
Overview
This package simplifies runtime permission handling in Flutter by providing:
- Riverpod integration for reactive state management
- Built-in dialogs (Material & Cupertino) for explanations, denials, and permanent denials
- Automatic permission status caching with TTL (3 seconds)
- Permanent denial detection with settings redirection
- PermissionBuilder and PermissionWrapper widgets
- Permission groups for managing related permissions together
- Custom explanation callbacks for complete UI control
- Smart rationale (Android-only) for better re-ask UX
Installation
Add to your pubspec.yaml:
dependencies:
permission_handler_package: ^2.1.2
Then run:
flutter pub get
Dependencies (auto-installed)
This package depends on:
permission_handler: ^12.0.1
riverpod: ^3.3.1
flutter_riverpod: ^3.3.1
flutter_screenutil: ^5.9.3
google_fonts: ^8.0.2
⚠️ Ensure your app doesn't pin incompatible major versions of
riverpod/flutter_riverpod.
Platform Configuration
Android Setup
Add only the permissions your app uses 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>
iOS Setup
Add usage description strings to ios/Runner/Info.plist for every permission you request:
<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</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</string>
<key>NSCalendarsUsageDescription</key>
<string>This app needs calendar access to schedule events</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 for personalized ads</string>
⚠️ Only include keys for permissions you actually request.
Initialization
Call PermissionHandler.initialize() once, before runApp():
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await PermissionHandler.initialize();
runApp(const ProviderScope(child: MyApp()));
}
Quick Start
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(colorScheme: ColorScheme.fromSeed(seedColor: 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()),
);
}
}
Permission Types & Groups
PermissionType Values
| PermissionType | Display Name |
|---|---|
camera |
Camera |
storage |
Storage |
photos |
Photos |
videos |
Videos |
audio |
Audio |
location |
Location |
locationAlways |
Location (Always) |
locationWhenInUse |
Location (While Using) |
microphone |
Microphone |
contacts |
Contacts |
notifications |
Notifications |
calendarWriteOnly |
Calendar (Write Only) |
calendarFullAccess |
Calendar (Full Access) |
reminders |
Reminders |
bluetooth |
Bluetooth |
sensors |
Sensors |
phone |
Phone |
sms |
SMS |
appTrackingTransparency |
App Tracking |
criticalAlerts |
Critical Alerts |
scheduleExactAlarm |
Exact Alarms |
ignoreBatteryOptimizations |
Battery Optimization |
manageExternalStorage |
External Storage |
systemAlertWindow |
System Alerts |
requestInstallPackages |
Install Packages |
accessNotificationPolicy |
Notification Policy |
PermissionGroup Values
| PermissionGroup | Members |
|---|---|
media |
storage, photos, videos, audio |
communication |
camera, microphone, contacts |
locationServices |
location, locationAlways, locationWhenInUse |
calendar |
calendarWriteOnly, calendarFullAccess, reminders |
bluetooth |
bluetooth |
sensors |
sensors |
phone |
phone, sms |
other |
(empty - catch-all) |
Usage Examples
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.isGranted && 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.isGranted);
if (allGranted) {
debugPrint('All communication permissions granted!');
}
},
child: const Text('Request Communication Permissions'),
);
}
}
3. Gate a Whole Screen with PermissionWrapper
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. Reactive Single Permission with PermissionBuilder
class CameraFeature extends ConsumerWidget {
const CameraFeature({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return PermissionBuilder(
permission: PermissionType.camera,
builder: (context, isGranted) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
isGranted ? Icons.camera_alt : Icons.camera_alt_outlined,
size: 80,
color: isGranted ? Colors.green : Colors.grey,
),
const SizedBox(height: 16),
Text(
isGranted ? 'Camera Ready' : 'Camera Permission Required',
style: Theme.of(context).textTheme.headlineSmall,
),
ElevatedButton(
onPressed: isGranted ? () => _openCamera() : null,
child: const Text('Take Photo'),
),
],
);
},
);
}
void _openCamera() {}
}
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: (isGranted) => ListTile(
leading: Icon(
isGranted ? Icons.check_circle : Icons.block,
color: isGranted ? Colors.green : Colors.red,
),
title: const Text('Camera'),
trailing: Text(isGranted ? '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 granted
7. Watch Multiple Permissions at Once
final statuses = ref.watch(
permissionsStatusProvider([PermissionType.camera, PermissionType.microphone]),
);
statuses.when(
data: (map) {
final cameraGranted = map[PermissionType.camera] ?? false;
final micGranted = map[PermissionType.microphone] ?? false;
},
loading: () {},
error: (_, _) {},
);
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.isGranted ? "granted" : "denied"}',
),
backgroundColor: event.result.isGranted ? 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.isGranted) {
// open the camera
}
}
10. 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
}
11. Reset Permission State
ref.read(permissionActionProvider.notifier).reset();
12. Open App Settings Manually
await ref.read(permissionManagerProvider).openAppSettings();
13. Clear the Permission Cache
final manager = ref.read(permissionManagerProvider);
manager.clearCache(PermissionType.camera);
manager.clearAllCache();
14. Custom Explanation Dialogs
class _MyWidgetState extends ConsumerState<MyWidget> {
@override
void initState() {
super.initState();
final actionNotifier = ref.read(permissionActionProvider.notifier);
actionNotifier.setPermissionExplanationCallback(
PermissionType.camera,
(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;
},
);
}
}
15. Smart Rationale (Android)
final manager = ref.read(permissionManagerProvider);
final result = await manager.requestPermission(
PermissionType.camera,
context: context,
useSmartRationale: true,
);
// Or check the rationale signal directly:
final shouldExplain = await manager.shouldShowRationale(PermissionType.camera);
16. Using PermissionManager Without Riverpod
final manager = PermissionManager();
final granted = await manager.isPermissionGranted(PermissionType.camera);
if (!granted) {
final result = await manager.requestPermission(
PermissionType.camera,
context: context,
);
}
API Reference
PermissionManager
PermissionManager() is a singleton.
| Method | Signature | Description |
|---|---|---|
checkPermissionsStatus |
(List<PermissionType>, {bool bypassCache}) → Future<Map<PermissionType, PermissionResult>> |
Checks status of multiple permissions. |
requestPermission |
(PermissionType, {BuildContext?, bool useSmartRationale}) → Future<PermissionResult> |
Requests a single permission. |
requestPermissions |
(List<PermissionType>, {BuildContext?}) → Future<Map<PermissionType, PermissionResult>> |
Requests multiple permissions sequentially. |
requestPermissionGroup |
(PermissionGroup, {BuildContext?}) → Future<Map<PermissionType, PermissionResult>> |
Requests all permissions in a group. |
isPermissionGranted |
(PermissionType) → Future<bool> |
Cached status check. |
isPermissionPermanentlyDenied |
(PermissionType) → Future<bool> |
Cached permanent denial check. |
shouldShowRationale |
(PermissionType) → Future<bool> |
Android-only OS signal. |
checkGroupPermissionsStatus |
(List<PermissionGroup>) → Future<Map<PermissionGroup, bool>> |
Checks if entire groups are granted. |
openAppSettings |
() → Future<void> |
Opens OS app settings. |
clearCache |
(PermissionType) → void |
Clears cached result for one permission. |
clearAllCache |
() → void |
Clears all cached results. |
setPermissionExplanationCallback |
(PermissionType, PermissionExplanationCallback?) → void |
Registers/removes custom explanation for a permission. |
setGroupExplanationCallback |
(PermissionGroup, PermissionGroupExplanationCallback?) → void |
Registers/removes custom explanation for a group. |
registerNavigatorKey |
(GlobalKey<NavigatorState>) → void |
Registers a navigator key for context fallback. |
unregisterNavigatorKey |
(GlobalKey<NavigatorState>) → void |
Unregisters a navigator key. |
markInitialized |
() → void |
Called internally by PermissionHandler.initialize(). |
onPermissionChanged |
Stream<PermissionChangeEvent> (getter) |
Fires when a permission's granted state changes. |
isDisposed / isInitialized |
bool (getters) |
Current manager state. |
PermissionActionNotifier
Accessed via ref.read(permissionActionProvider.notifier).
| Method | Signature | Description |
|---|---|---|
initializeRequiredPermissions |
({required BuildContext context, required List<PermissionType> requiredPermissions, List<PermissionGroup>? requiredGroups, bool showInitialScreen = true, String? title, String? message}) → Future<bool> |
Full onboarding flow. |
requestSinglePermission |
(PermissionType, {BuildContext?}) → Future<PermissionResult> |
Always re-requests. |
requestIfNeeded |
(PermissionType, {BuildContext?}) → Future<PermissionResult> |
Skips if already granted. |
requestPermissionGroup |
(PermissionGroup, {BuildContext?}) → Future<Map<PermissionType, PermissionResult>> |
Requests a group. |
autoInitialize |
() → Future<void> |
Checks and caches all permissions. Called automatically. |
setPermissionExplanationCallback |
(PermissionType, PermissionExplanationCallback?) → void |
Pass-through to PermissionManager. |
setGroupExplanationCallback |
(PermissionGroup, PermissionGroupExplanationCallback?) → void |
Pass-through to PermissionManager. |
reset |
() → void |
Clears Riverpod state and cache. |
Providers
| Provider | Type | Description |
|---|---|---|
permissionManagerProvider |
Provider<PermissionManager> |
The singleton manager instance. |
permissionStateProvider |
ChangeNotifierProvider<PermissionNotifier> |
Full permission state map. |
permissionActionProvider |
StateNotifierProvider<PermissionActionNotifier, AsyncValue<void>> |
Performs requests. Auto-runs autoInitialize(). |
permissionStatusProvider |
FutureProvider.family<bool, PermissionType> |
Read-only single permission status. |
permissionsStatusProvider |
FutureProvider.family<Map<PermissionType, bool>, List<PermissionType>> |
Read-only multiple permissions status. |
permissionGroupStatusProvider |
FutureProvider.family<bool, PermissionGroup> |
Read-only group status. |
PermissionResult
| Member | Type | Description |
|---|---|---|
permission |
PermissionType |
The permission. |
isGranted |
bool |
Whether granted. |
isPermanentlyDenied |
bool |
Whether permanently denied. |
status |
PermissionStatus |
Raw status from permission_handler. |
timestamp |
DateTime |
When the result was created. |
isDenied |
bool (getter) |
Delegates to status.isDenied. |
isLimited |
bool (getter) |
Delegates to status.isLimited (iOS). |
isRestricted |
bool (getter) |
Delegates to status.isRestricted (iOS). |
PermissionState
Exposed via ref.watch(permissionStateProvider).state.
| Member | Type | Description |
|---|---|---|
permissions |
Map<PermissionType, PermissionResult> |
All permission results. |
isInitialized |
bool |
Whether initialized. |
isLoading |
bool |
Loading state. |
error |
String? |
Error message. |
isPermissionGranted(PermissionType) |
bool |
Check if a permission is granted. |
isPermissionPermanentlyDenied(PermissionType) |
bool |
Check if permanently denied. |
getGrantedPermissions() |
List<PermissionType> |
List of granted permissions. |
getDeniedPermissions() |
List<PermissionType> |
List of denied permissions. |
Widgets
| Widget | Props | Purpose |
|---|---|---|
PermissionWrapper |
child, requiredPermissions, loadingWidget?, permissionDeniedWidget?, title?, message?, onPermissionsGranted?, onPermissionsDenied? |
Gates a whole screen behind permissions. |
PermissionBuilder |
permission, builder, loadingWidget?, deniedWidget? |
Reactive single-permission builder. |
PermissionInitialDialog |
permissions, title?, message? |
Default "why we need this" dialog. |
PermissionDeniedDialog |
permissions |
Shown after non-permanent denial. |
PermissionPermanentDialog |
permissions, title?, message? |
Shown when permanently denied. |
Theming
All built-in widgets use Theme.of(context).colorScheme (Android) and CupertinoTheme.of(context) (iOS). Text uses GoogleFonts.urbanist.
MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFFFFE000)),
),
home: const MyApp(),
)
Wrap your app in ScreenUtilInit for proper scaling:
ScreenUtilInit(
designSize: const Size(375, 812),
builder: (context, child) => MaterialApp(home: child),
child: const HomePage(),
)
Troubleshooting
Permission Calls Do Nothing on Startup
Ensure 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
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.
Cache Seems Stale
The manager auto-rechecks on app resume. For immediate refresh:
final results = await manager.checkPermissionsStatus(
[PermissionType.camera],
bypassCache: true,
);
PermissionBuilder Shows Wrong Button After Returning from Settings
It automatically rechecks. If still stale:
ref.invalidate(permissionStatusProvider(PermissionType.camera));
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 — see example 16.
Q: What's the difference between requestSinglePermission and requestIfNeeded?
requestSinglePermission always shows the system dialog. requestIfNeeded checks first and returns immediately if already granted.
Q: How do I customize the explanation dialog?
Use setPermissionExplanationCallback / setGroupExplanationCallback — see example 14.
Q: How long are results cached?
3 seconds. Pass bypassCache: true for a fresh read.
Q: What happens if I call methods before initialization?
They're queued and run automatically after initialization completes.
Q: Does openAppSettings() tell me if it actually opened?
No. Check the permission status after the user returns to the app.
License
MIT License — see LICENSE for details.
Libraries
- core/permissions/models/permission_result
- core/permissions/models/permission_type
- core/permissions/permission_manager
- core/permissions/permission_provider
- core/permissions/permission_state
- core/permissions/widgets/permission_builder
- core/permissions/widgets/permission_denied_dialog
- core/permissions/widgets/permission_initial_dialog
- core/permissions/widgets/permission_permanent_dialog
- core/permissions/widgets/permission_wrapper
- permission_handler_package
- Permission Handler Package - Fully Automatic No manual handling required. Just add to your pubspec.yaml and it works.