✅ 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

pub package License: MIT Flutter Dart

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

  • Riverpod Integration — Reactive permission state with ref.watch (optional)
  • Direct Usage — Use PermissionManager without any state management
  • Unambiguous UI StatesPermissionUiState enum eliminates boolean-combination bugs
  • Race-Free Settings Navigation — Subscribe-before-launch, real AppLifecycleState resume 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 StateisSufficient treats granted, limited, and provisional as usable
  • Comprehensive Error HandlingFlutterError.reportError for debugging
  • Legacy Compatibility — Popup dialogs still available for existing apps
  • 40+ Permission Types — Full coverage of permission_handler permissions
  • 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 PermissionManager API without Riverpod, you don't need riverpod or flutter_riverpod in 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 PermissionManager singleton
  • Calls markInitialized() to flush queued operations
  • Prepares the package for use

Note: Wrap your app in a ProviderScope only if you plan to use Riverpod providers.


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(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.other has no members. Calling requestPermissionGroup(PermissionGroup.other) is a silent no-op. In debug builds, a warning is logged. Request individual permissions directly instead.


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.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: permissionsStatusProvider uses PermissionTypeListKey, not a bare List, 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 screen
  • message: 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',
      ),
    ));
  }
}

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 popup
  • PermissionDeniedDialog — Post-denial popup
  • PermissionPermanentDialog — Permanent denial popup

When they appear:

  • Direct calls to PermissionManager.requestPermission()
  • PermissionActionNotifier.showLegacyPermanentDenialDialog()

They do NOT appear in:

  • initializeRequiredPermissions()
  • PermissionWrapper
  • PermissionBuilder

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.