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.