permission_handler_package 2.0.5 copy "permission_handler_package: ^2.0.5" to clipboard
permission_handler_package: ^2.0.5 copied to clipboard

A professional Flutter package for handling permissions automatically with Riverpod state management, retry logic, and beautiful UI dialogs.

Permission Handler Package #

pub package License: MIT

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 #

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.

1
likes
150
points
159
downloads

Documentation

Documentation
API reference

Publisher

unverified uploader

Weekly Downloads

A professional Flutter package for handling permissions automatically with Riverpod state management, retry logic, and beautiful UI dialogs.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter, flutter_riverpod, flutter_screenutil, google_fonts, permission_handler, riverpod

More

Packages that depend on permission_handler_package