🧠 smart_permission

Pub.dev Badge Build Badge MIT License Flutter Badge

πŸš€ An opinionated wrapper around permission_handler that makes runtime permissions effortless.
Request permissions with one API β€” smart dialogs, adaptive styles, and complete UX flow handling built-in.

Smart Permission


✨ Why smart_permission?

Most apps spend unnecessary time handling permission logic manually. smart_permission takes care of the entire flow β€” from first ask to permanently denied β€” automatically, with adaptive dialogs and analytics tracking.

πŸ’‘ What makes it different?

  • βœ… One-line permission requests (single or batch)
  • βœ… Handles denied, permanently denied, and restricted flows automatically
  • βœ… Rich results (SmartPermissionResult) that tell you why a request failed
  • βœ… Waits for the user to return from Settings before re-checking
  • βœ… Adaptive Material / Cupertino / Adaptive dialogs
  • βœ… Built-in titles & descriptions (with custom overrides) + full localization
  • βœ… Global theming, analytics hooks, and custom dialogs
  • βœ… Testable: inject a fake gateway to widget-test your permission UX
  • βœ… Re-exports Permission β€” no need for multiple imports

🧩 Feature Highlights

Feature Description
πŸ” Easy API SmartPermission.request() for one or many permissions
🎯 Rich Results requestResult() returns granted/denied/permanentlyDenied/...
🎨 Adaptive Dialogs Material / Cupertino / Platform adaptive support
🧭 Auto Flows Denied, permanently denied, and restricted handled automatically
πŸ’¬ Rationale First Optionally explain before the one-shot native prompt
🌍 Localization Every built-in string replaceable via SmartPermissionStrings
🧱 Central Configuration Global themes, titles, descriptions, analytics, navigator key
🧩 Custom Dialog Builders Build your own dialog UI if you need full control
πŸ“Š Analytics Hooks requested / granted / denied / permanently denied / restricted
πŸ§ͺ Test-Friendly Swap the platform gateway with a fake in widget tests

πŸ–₯️ Platform Support

Android iOS Web (incl. WASM) Windows macOS Linux
βœ… βœ… βœ… βœ… ❌ ❌

macOS and Linux are not supported because the underlying permission_handler plugin has no implementation for them.


βš™οΈ Installation

Add to your pubspec.yaml:

dependencies:
  smart_permission: ^1.0.0

Android note: smart_permission 1.0.0+ depends on permission_handler 13, which requires your app to build with compileSdk 37. Flutter's default is still 36, so set it explicitly in android/app/build.gradle(.kts):

android {
    compileSdk = 37
    // ...
}

Then import:

import 'package:smart_permission/smart_permission.dart';

⚑ Quick Start Example

final ok = await SmartPermission.request(
  context,
  permission: Permission.camera,
  style: PermissionDialogStyle.adaptive,
  description: 'We need camera to scan QR codes.',
);

Or request multiple:

final result = await SmartPermission.requestMultiple(
  context,
  permissions: [
    Permission.camera,
    Permission.microphone,
  ],
);

Automatic Dialog Flow

State Behavior
First time Shows native system sheet (optionally after a rationale)
Denied Shows rationale dialog with retry
Permanently denied Shows β€œOpen Settings” dialog, waits for return, re-checks
Restricted Shows an informational dialog (settings can't fix restricted)

Need to know why it failed? Use the result API

final result = await SmartPermission.requestResult(
  context: context,
  permission: Permission.camera,
);

if (result.canProceed) {
  // granted, limited, or provisional
  openCamera();
} else if (result == SmartPermissionResult.permanentlyDenied) {
  // Show a settings shortcut somewhere in your UI.
  showSettingsShortcut();
} else if (result == SmartPermissionResult.restricted) {
  // Blocked by the OS (e.g. parental controls) β€” hide the feature.
  hideCameraFeature();
} else {
  // denied or error β€” you can ask again later.
}

requestMultipleResults does the same for batches β€” one native batch flow, one combined rationale dialog, and one combined settings dialog instead of a dialog per permission.

On iOS you only get one native ask β€” explain first:

// Per call:
await SmartPermission.request(context,
    permission: Permission.camera, showRationaleFirst: true);

// Or globally:
SmartPermission.config.showRationaleBeforeRequest = true;

No BuildContext? Use a navigator key

final navKey = GlobalKey<NavigatorState>();

MaterialApp(navigatorKey: navKey, ...);
SmartPermission.config.navigatorKey = navKey;

// Anywhere, without a context:
final result = await SmartPermission.requestResult(
  permission: Permission.camera,
);

🎨 Configuration & Customization

You can configure SmartPermission globally at app startup or dynamically at runtime.

Global Setup

SmartPermission.config
  ..brightness = Brightness.light
  ..primaryColor = Colors.indigo
  ..analytics = InMemoryPermissionAnalyticsTracker();

Toggle Theme & Primary Color (as in example app)

SmartPermission.config.brightness =
    isDark ? Brightness.dark : Brightness.light;

SmartPermission.config.primaryColor = selectedPrimaryColor;

🧠 Custom Texts Per Permission

Provide your own titles and messages for each permission:

SmartPermission.config
  ..titleProvider = (p) {
    if (p == Permission.camera) return 'Camera Access Needed';
    if (p == Permission.microphone) return 'Microphone Access Needed';
    if (p == Permission.locationWhenInUse) return 'Location Access Needed';
    return null;
  }
  ..descriptionProvider = (p) {
    if (p == Permission.camera) return 'We need the camera to scan QR codes.';
    if (p == Permission.microphone) return 'We need the microphone for voice features.';
    if (p == Permission.locationWhenInUse) return 'We use your location to show nearby stores.';
    return null;
  };

🧩 Custom Dialog Builder

Want your own UI (like a bottom sheet)? You can override the default dialog completely.

SmartPermission.config.customDialogBuilder = (
  context, {
  required style,
  required title,
  required message,
  required primaryText,
  required secondaryText,
}) async {
  return showModalBottomSheet<bool>(
    context: context,
    isScrollControlled: true,
    backgroundColor: Theme.of(context).colorScheme.surface,
    shape: const RoundedRectangleBorder(
      borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
    ),
    builder: (ctx) {
      final cs = Theme.of(ctx).colorScheme;
      return Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Row(children: [
              Icon(Icons.privacy_tip_outlined, color: cs.primary),
              const SizedBox(width: 12),
              Expanded(child: Text(title, style: Theme.of(ctx).textTheme.titleLarge)),
            ]),
            const SizedBox(height: 12),
            Text(message),
            const SizedBox(height: 20),
            Row(
              children: [
                Expanded(
                  child: OutlinedButton(
                    onPressed: () => Navigator.pop(ctx, false),
                    child: Text(secondaryText),
                  ),
                ),
                const SizedBox(width: 12),
                Expanded(
                  child: FilledButton(
                    onPressed: () => Navigator.pop(ctx, true),
                    child: Text(primaryText),
                  ),
                ),
              ],
            ),
          ],
        ),
      );
    },
  );
};

🌍 Localization

Every built-in string can be replaced (unset ones keep their English default):

SmartPermission.config.strings = const SmartPermissionStrings(
  allow: 'Autoriser',
  notNow: 'Plus tard',
  openSettings: 'Ouvrir les rΓ©glages',
  blockedMessage: 'Autorisation dΓ©sactivΓ©e. Activez-la dans les rΓ©glages.',
);

Combine with titleProvider/descriptionProvider for localized per-permission texts.


πŸ“Š Analytics Tracking

Track user behavior for better insights or debugging. Extend PermissionAnalyticsTracker (don't implement it) so future hooks won't break your tracker:

class MyAnalytics extends PermissionAnalyticsTracker {
  @override
  void onRequested(Permission permission) => log('requested: $permission');

  @override
  void onGranted(Permission permission) => log('granted: $permission');

  @override
  void onDenied(Permission permission) => log('denied: $permission');

  @override
  void onPermanentlyDenied(Permission permission) => log('blocked: $permission');

  @override
  void onRestricted(Permission permission) => log('restricted: $permission');
}

SmartPermission.config.analytics = MyAnalytics();

The example app uses a small tracker to log permission denials in real-time within the UI.


🚨 Error Reporting

Platform-call failures are logged with debugPrint and surface as SmartPermissionResult.error. Forward them to your crash reporter:

SmartPermission.config.onError = (error, stack) =>
    FirebaseCrashlytics.instance.recordError(error, stack);

πŸ§ͺ Testing Your Permission Flows

Widget-test your app's permission UX without platform channels by swapping the gateway:

class FakeGateway implements SmartPermissionGateway {
  // Return whatever statuses your test needs...
}

SmartPermission.config.gateway = FakeGateway();
// ...run your flow, then:
SmartPermission.config.resetToDefaults();

🧩 Example App Features

The included /example project demonstrates:

  • πŸ”„ Theme toggle (Light/Dark)
  • 🎨 Primary color cycling (Indigo / Teal / Orange)
  • πŸ“± Dialog style selection (Material, Cupertino, Adaptive)
  • 🧠 Custom title/description per permission
  • πŸͺŸ Custom bottom-sheet dialog builder
  • πŸ“Š Real-time analytics tracking for denied states
  • πŸ“¦ Batch permission requests

🧠 Tips & Notes

  • Some Android permissions (e.g., manageExternalStorage, systemAlertWindow) open system settings instead of a dialog.
  • Android 17 adds Permission.accessLocalNetwork (declare android.permission.ACCESS_LOCAL_NETWORK in your manifest) β€” supported out of the box with built-in dialog texts.
  • On Android 13+, use Permission.photos, Permission.videos, or Permission.audio instead of deprecated storage permissions.
  • locationAlways often requires first granting locationWhenInUse.
  • Use SmartPermission.requestMultiple() for grouped requests.

πŸ”€ Migrating from 0.x

1.0.0 keeps the 0.x API working β€” request(...) and requestMultiple(...) have the same signatures and return types. Notes:

  • Android: permission_handler 13 requires building with compileSdk 37.
  • Analytics: if your tracker used implements PermissionAnalyticsTracker, switch to extends (new hooks were added: onRequested, onGranted, onRestricted).
  • Behavior: requestMultiple now runs one native batch flow with combined dialogs instead of a full dialog flow per permission; the settings flow now waits for the user to return from Settings before re-checking (previously it re-checked immediately and usually reported false).
  • New APIs (opt-in): requestResult, requestMultipleResults, SmartPermissionResult, SmartPermissionStrings, showRationaleFirst, config.navigatorKey, config.onError, config.gateway.

πŸ‘€ Author

Created with ❀️ by Jaimin Kavathia πŸ’Ό LinkedIn β€’ πŸ™ GitHub


πŸ“œ License

Licensed under the MIT License. Free for personal and commercial use.


⭐ If you like this package, give it a star on GitHub & pub.dev!

smart_permission β€” because handling permissions shouldn’t be a hassle.

Libraries

smart_permission