Skip to main content

okapi 1.0.0 copy "okapi: ^1.0.0" to clipboard
okapi: ^1.0.0 copied to clipboard

Complete permission manager for Flutter: guided flows, customisable UI, feature-based asking, OEM background guides, Doctor and Sync tools.

Okapi: the complete permission manager for Flutter

Ask once. Ask right. Never get stuck.
The complete permission manager for Flutter on Android and iOS.

pub.dev Android | iOS License


Okapi checks, requests and guides users through every Android and iOS permission. It ships the whole flow most apps rebuild by hand: a friendly explanation before the system dialog, a retry after a refusal, a clear path to Settings when a permission is blocked, and an automatic re-check when the user comes back. All of it with beautiful, fully customisable UI, or your own widgets.

final result = await Okapi.ask(context, OkapiPermission.camera);
if (result.isGranted) openCamera();

Why Okapi #

Common problem What Okapi does
The one-shot system dialog is wasted because users don't know why you ask Shows a rationale screen first, so "Not now" costs nothing
"Permanently denied" apps get stuck forever Detects it, explains how to fix it, opens Settings and re-checks on return
Android reports "permanently denied" when the user just dismissed the dialog Compares the rationale flag before and after each request to tell them apart
A request for permissions is already running crashes Every request and flow is queued automatically
iOS crashes when an Info.plist key is missing; Android fails silently Okapi checks first and throws an OkapiException with the exact lines to paste
Background location requested in the wrong order Foreground first, then background, handled for you
The ATT dialog never shows on app launch Okapi waits until the app is active
Location or Bluetooth is switched off Okapi notices and offers to turn it on
Status goes stale after the user changes Settings Every known permission is re-checked when the app resumes, with a live stream
Packages declare permissions you never use Okapi declares none; you add only what you need
Hard to test FakeOkapiPlatform for unit tests and a debug panel to simulate any state on a device
Missing or extra manifest / Info.plist entries found only after a store rejection dart run okapi:doctor checks everything, dart run okapi:sync writes it for you
Notifications and alarms die on Xiaomi, vivo, OPPO, Samsung and other phones A brand-aware guide opens the right hidden autostart and battery screens
Android silently removes permissions from apps unused for a few months Okapi spots removed permissions and offers to turn auto-reset off
Apps nag users until they refuse forever An on-device ask policy: cooldowns, first-launch skip, per-session limit
Code juggles many permissions for one feature Ask for a feature; Okapi returns full, partial or unavailable

Also included: 37 permissions, a ready-made Privacy Center screen, Android special app-access screens, iOS limited/provisional states, Hindi and Marathi translations, an Apple privacy manifest, zero third-party dependencies.

Contents #

  1. Install
  2. Platform setup
  3. Quick start
  4. Statuses
  5. Guided flows
  6. Low-level API
  7. Customise the UI
  8. Ready-made widgets
  9. Ask for features
  10. Ask policy
  11. Background reliability on Indian phones
  12. Auto-reset protection
  13. Privacy Center
  14. Okapi Doctor and Okapi Sync
  15. Debug panel
  16. Testing
  17. Permission reference
  18. App Store and Play tips
  19. Troubleshooting
  20. Legal

Install #

dependencies:
  okapi: ^1.0.0
flutter pub get

Requires Flutter 3.27+, Android 5.0+ (API 21), iOS 12+.

Platform setup #

Android #

Add only the permissions you use to android/app/src/main/AndroidManifest.xml, above <application>:

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

Set compileSdk to at least 35 in android/app/build.gradle. See the Permission reference for every name. Not sure? Call OkapiDeclarations.hint(OkapiPermission.photos, isIOS: false) to print the exact lines.

iOS #

Add a usage description for each permission to ios/Runner/Info.plist. Write real, specific reasons: Apple rejects vague ones.

<key>NSCameraUsageDescription</key>
<string>Scan receipts and take profile photos.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Show stores near you.</string>

Optional but recommended: strip the APIs you don't use (see App Store tips).

Quick start #

import 'package:okapi/okapi.dart';

void main() {
  Okapi.configure(const OkapiConfig()); // Optional: sensible defaults.
  runApp(const MyApp());
}

// In any widget:
Future<void> scan(BuildContext context) async {
  final result = await Okapi.ask(
    context,
    OkapiPermission.camera,
    reason: 'We use the camera to scan your receipts.',
  );
  if (result.isGranted) {
    // Start scanning.
  }
}

Statuses #

OkapiStatus Meaning canProceed
granted Fully allowed ✅
limited Partly allowed (selected photos, approximate location, limited contacts) ✅
provisional iOS quiet notifications ✅
notApplicable No permission needed on this platform/version ✅
denied Not allowed yet; you can ask ❌
permanentlyDenied Can only be changed in Settings ❌
restricted Blocked by parental/MDM controls; the user can't change it ❌

Use status.canProceed for "can I use the feature now?".

Guided flows #

One permission #

final r = await Okapi.ask(context, OkapiPermission.location);

final message = switch (r.outcome) {
  OkapiOutcome.granted => 'Ready',
  OkapiOutcome.dismissedRationale => 'Tapped "Not now" on Okapi\'s screen',
  OkapiOutcome.denied => 'Refused in the system dialog',
  OkapiOutcome.permanentlyDenied => 'Blocked; did not go to Settings',
  OkapiOutcome.settingsReturnedDenied => 'Went to Settings, still off',
  OkapiOutcome.restricted => 'Blocked by device restrictions',
  OkapiOutcome.serviceDisabled => 'Location / Bluetooth is switched off',
  OkapiOutcome.deferredByPolicy => 'Postponed by the ask policy',
};

The flow: declared? → service on? → already granted? → rationale → system dialog → retry → Settings → re-check on return. Every step can be turned off:

await Okapi.ask(
  context,
  OkapiPermission.microphone,
  rationaleMode: OkapiRationaleMode.whenRecommended,
  maxRetries: 0,
  offerSettings: true,
  settingsRightAfterRefusal: false,
  checkService: false,
);

Several permissions #

final results = await Okapi.askAll(context, [
  OkapiPermission.camera,
  OkapiPermission.microphone,
], reasons: {
  OkapiPermission.camera: 'See each other on video calls.',
  OkapiPermission.microphone: 'Hear each other on calls.',
});

One explanation lists them all, the system dialogs follow one by one, and blocked ones share one Settings prompt.

Low-level API #

final s = await Okapi.status(OkapiPermission.contacts);
final r = await Okapi.request(OkapiPermission.contacts);
final m = await Okapi.requestMany([OkapiPermission.camera, OkapiPermission.photos]);
final why = await Okapi.shouldShowRationale(OkapiPermission.camera); // Android
final svc = await Okapi.serviceStatus(OkapiPermission.location);
await Okapi.openAppSettings();
await Okapi.openServiceSettings(OkapiPermission.bluetooth);

Okapi.watch(OkapiPermission.notification).listen((s) => print(s));
Okapi.statusChanges.listen((change) => print(change));

Customise the UI #

Theme #

Okapi blends into your app's ColorScheme by default. Pick a preset or tweak anything:

Okapi.configure(OkapiConfig(
  theme: OkapiThemeData.okapiDark().copyWith(radius: 20, blurSigma: 10),
));

Presets: fromColorScheme, okapiLight, okapiDark, mono. You can also add OkapiThemeData to ThemeData.extensions or wrap a subtree in OkapiTheme.

Replace the icon badge with any widget (Lottie, image, illustration):

OkapiThemeData.okapiLight().copyWith(
  visualBuilder: (context, permission, theme) =>
      Image.asset('assets/${permission.name}.png', height: 120),
)

Presentation style #

OkapiConfig(presenter: const OkapiSheetPresenter());      // Default bottom sheet
OkapiConfig(presenter: const OkapiDialogPresenter());     // Centred dialog
OkapiConfig(presenter: const OkapiFullScreenPresenter()); // Full page
OkapiConfig(presenter: const OkapiNoUiPresenter());       // Logic only, no Okapi screens
OkapiConfig(presenter: const OkapiMixedPresenter(byKind: {
  OkapiPromptKind.rationale: OkapiFullScreenPresenter(),
  OkapiPromptKind.settings: OkapiDialogPresenter(),
}));

Your own widgets, Okapi's logic #

Okapi.configure(OkapiConfig(
  presenter: OkapiCustomPresenter(
    style: OkapiPresentationStyle.dialog,
    builder: (context, prompt, c) => Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Icon(c.iconOf(prompt.permission), size: 48),
        Text(prompt.title),
        Text(prompt.message),
        FilledButton(onPressed: c.accept, child: Text(prompt.primaryLabel)),
        if (prompt.secondaryLabel != null)
          TextButton(onPressed: c.decline, child: Text(prompt.secondaryLabel!)),
      ],
    ),
  ),
));

The OkapiPromptController gives you accept(), decline(), openSettings(), status(), nameOf(), iconOf(), strings and theme. Use builders: {OkapiPromptKind.settings: ...} to customise only some screens, wrapInFrame: false to draw the whole container, or OkapiFunctionPresenter to show your own route entirely.

Text and languages #

Okapi.configure(const OkapiConfig(strings: OkapiStrings.hindi)); // or .marathi

Okapi.configure(OkapiConfig(
  strings: OkapiStrings(
    permissionReasons: {OkapiPermission.camera: 'Scan QR codes to pay.'},
    rationaleAllow: 'Sounds good',
  ),
  promptOverride: (p) => p.kind == OkapiPromptKind.settings
      ? p.copyWith(title: 'One quick switch in Settings')
      : p,
));

Templates support {permission}, {reason} and {service}.

Analytics hooks #

class MyObserver extends OkapiObserver {
  @override
  void onResult(OkapiResult r) => analytics.log('perm_${r.permission.name}', r.outcome.name);
}
Okapi.configure(OkapiConfig(observers: [MyObserver()]));

Ready-made widgets #

// Show the child only when allowed; otherwise a polished "Allow" card.
OkapiPermissionGate(
  permission: OkapiPermission.camera,
  reason: 'Point at a QR code to pay.',
  child: const ScannerView(),
);

// Build anything from the live status.
OkapiPermissionBuilder(
  permission: OkapiPermission.notification,
  builder: (context, status, ask) => SwitchListTile(
    title: const Text('Notifications'),
    value: status?.canProceed ?? false,
    onChanged: (_) => ask(),
  ),
);

// A row with icon, name, reason and an Allow button or status chip.
const OkapiPermissionTile(permission: OkapiPermission.contacts);

// A full onboarding screen.
OkapiOnboarding(
  permissions: const [OkapiPermission.notification, OkapiPermission.location],
  requiredPermissions: const [OkapiPermission.notification],
  onComplete: (statuses) => Navigator.of(context).pushReplacementNamed('/home'),
);

Ask for features #

Describe what a feature needs; Okapi asks in one flow, marks optional permissions, and tells you how much can run.

const videoCall = OkapiFeature(
  id: 'video_call',
  name: 'Video calls',
  required: [OkapiPermission.camera, OkapiPermission.microphone],
  optional: [OkapiPermission.bluetoothConnect],
);

final r = await Okapi.askFeature(context, videoCall);
switch (r.level) {
  case OkapiFeatureLevel.full:        startCall();
  case OkapiFeatureLevel.partial:     startCall(headset: false);
  case OkapiFeatureLevel.unavailable: showWhyCallsNeedCamera();
}

Also: Okapi.featureStatus(feature) (no UI), OkapiFeatureGate(feature:, child:, partialChild:) and OkapiFeatureBuilder.

Ask policy #

Stop nagging with rules that run on the device only:

Okapi.configure(const OkapiConfig(askPolicy: OkapiAskPolicy.recommended));

recommended skips the first launch, waits 2 days after "Not now", 7 days after a refusal, 14 days after the user declined Settings, and allows 2 Okapi prompts per session. Tune any rule with OkapiAskPolicy(...) or .copyWith(...). A postponed flow returns OkapiOutcome.deferredByPolicy with the reason in result.policy. When the user explicitly asks (taps a switch), pass force: true.

Background reliability on Indian phones #

Xiaomi, Redmi, POCO, vivo, iQOO, OPPO, realme, OnePlus, Samsung, TECNO, Infinix, itel, Huawei, Honor, ASUS and Nokia phones can stop apps in the background, so notifications and alarms arrive late or never. Okapi detects the brand, shows brand-specific steps, opens the right hidden autostart or battery screen (with safe fallbacks), and asks the user to confirm.

final r = await Okapi.askBackgroundReliability(
  context,
  reason: 'So your medicine reminders ring on time.',
);
if (r.ready) scheduleReminders();

Ask at a meaningful moment (after the user creates their first reminder), not at launch. Phones don't let apps read the autostart switch, so autostartConfirmed is the user's answer. Declare REQUEST_IGNORE_BATTERY_OPTIMIZATIONS if your app qualifies under Play policy to also handle Android's battery optimisation directly. Low level: Okapi.deviceInfo(), Okapi.oemScreens(), Okapi.openOemScreen(...), Okapi.backgroundStatus(), Okapi.needsBackgroundSetup(). Override the steps per brand with OkapiStrings(oemSteps: {...}).

Auto-reset protection #

Android removes permissions from apps that aren't opened for a few months. Okapi remembers which permissions were allowed, spots removed ones, and offers to turn auto-reset off:

// Gentle: only when something was actually removed.
await Okapi.protectFromAutoReset(context, onlyIfRevoked: true);

final removed = await Okapi.revokedSinceLastLaunch();
final status = await Okapi.autoResetStatus(); // enabled, exempt, notAvailable

Privacy Center #

A ready-made screen for your app's settings: features and permissions with live status and reasons, Allow and Manage buttons, plus background and auto-reset cards on Android.

Navigator.of(context).push(MaterialPageRoute(
  builder: (_) => OkapiPrivacyCenter(
    features: [videoCall],
    permissions: [OkapiPermission.notification],
    onOpenPrivacyPolicy: () => openMyPolicyPage(),
  ),
));

Set asPage: false to embed it in your own page.

Okapi Doctor and Okapi Sync #

Run these from your app folder.

dart run okapi:doctor          # check everything
dart run okapi:sync --dry-run  # preview the fixes
dart run okapi:sync            # write them

Doctor scans lib/ for OkapiPermission usage and reports:

  • permissions missing from AndroidManifest.xml or Info.plist, with the exact lines to paste;
  • declared permissions your code never asks for;
  • Google Play restricted permissions (SMS, call log, all files, background location, exact alarms, photos and videos...);
  • vague or placeholder iOS usage texts Apple tends to reject;
  • iOS permission APIs that are compiled in but unused, or excluded but used;
  • compileSdk and iOS deployment target problems.

Add --json for CI and --strict to fail on warnings.

Sync writes what's missing into a marked region (okapi:begin / okapi:end) of AndroidManifest.xml, Info.plist and the Podfile. It never touches anything outside that region, keeps the Info.plist texts you edit, and removes entries when you stop using a permission. iOS texts start as TODO: placeholders; Doctor reports them until you replace them.

If your code chooses permissions at runtime (OkapiPermission.values, .byName), list them in pubspec.yaml so both tools can see them:

okapi:
  permissions: [contacts, calendarFullAccess]
  ios_keep: [BLUETOOTH]   # never exclude these iOS APIs
  manage_podfile: true

Debug panel #

Simulate any permission state on a real device, without reinstalling or resetting the phone:

MaterialApp(
  builder: (context, child) => OkapiDebugPanel(child: child!),
);

A draggable button opens the panel. Set any permission to granted, denied, blocked, limited or restricted; switch Location or Bluetooth off; make the system dialog "allow"; preview the background guide for Xiaomi, Samsung, vivo and more; simulate auto-reset; and clear the ask history. It is active only in debug builds and returns your app untouched in profile and release builds.

Testing #

import 'package:okapi/okapi.dart';
import 'package:okapi/testing.dart';

setUp(() {
  OkapiPlatform.instance = FakeOkapiPlatform(
    statuses: {OkapiPermission.camera: OkapiStatus.denied},
    requestResults: {OkapiPermission.camera: OkapiStatus.granted},
  );
});

Permission reference #

OkapiPermission Android manifest iOS Info.plist
camera CAMERA NSCameraUsageDescription
microphone RECORD_AUDIO NSMicrophoneUsageDescription
speech RECORD_AUDIO NSSpeechRecognitionUsageDescription
photos READ_MEDIA_IMAGES, READ_MEDIA_VISUAL_USER_SELECTED, READ_EXTERNAL_STORAGE (max 32) NSPhotoLibraryUsageDescription
photosAddOnly n/a NSPhotoLibraryAddUsageDescription
videos READ_MEDIA_VIDEO, READ_EXTERNAL_STORAGE (max 32) NSPhotoLibraryUsageDescription
audio READ_MEDIA_AUDIO, READ_EXTERNAL_STORAGE (max 32) n/a
storage READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE (max 28) n/a
manageExternalStorage MANAGE_EXTERNAL_STORAGE (special) n/a
mediaLocation ACCESS_MEDIA_LOCATION n/a
mediaLibrary n/a NSAppleMusicUsageDescription
location, locationWhenInUse ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION NSLocationWhenInUseUsageDescription
locationAlways + ACCESS_BACKGROUND_LOCATION + NSLocationAlwaysAndWhenInUseUsageDescription
notification POST_NOTIFICATIONS none
criticalAlerts n/a none (needs Apple entitlement)
accessNotificationPolicy ACCESS_NOTIFICATION_POLICY (special) n/a
contacts READ_CONTACTS, WRITE_CONTACTS NSContactsUsageDescription
calendarFullAccess READ_CALENDAR, WRITE_CALENDAR NSCalendarsFullAccessUsageDescription
calendarWriteOnly WRITE_CALENDAR NSCalendarsWriteOnlyAccessUsageDescription
reminders n/a NSRemindersFullAccessUsageDescription
phone READ_PHONE_STATE, CALL_PHONE, … (any declared) n/a
sms SEND_SMS, READ_SMS, … (any declared) n/a
sensors BODY_SENSORS NSMotionUsageDescription
sensorsAlways + BODY_SENSORS_BACKGROUND n/a
activityRecognition ACTIVITY_RECOGNITION NSMotionUsageDescription
bluetooth BLUETOOTH_SCAN, BLUETOOTH_CONNECT NSBluetoothAlwaysUsageDescription
bluetoothScan / Connect / Advertise the matching BLUETOOTH_* NSBluetoothAlwaysUsageDescription
nearbyWifiDevices NEARBY_WIFI_DEVICES n/a
appTrackingTransparency n/a NSUserTrackingUsageDescription
ignoreBatteryOptimizations REQUEST_IGNORE_BATTERY_OPTIMIZATIONS (special) n/a
systemAlertWindow SYSTEM_ALERT_WINDOW (special) n/a
requestInstallPackages REQUEST_INSTALL_PACKAGES (special) n/a
scheduleExactAlarm SCHEDULE_EXACT_ALARM or USE_EXACT_ALARM (special) n/a
backgroundRefresh n/a none (read-only)

"n/a" returns OkapiStatus.notApplicable. "Special" opens a system screen; Okapi re-checks when the user returns.

App Store and Play tips #

iOS: exclude APIs you don't use. Apple may flag apps that link permission APIs without a matching usage description. Okapi compiles every family by default; exclude the rest in ios/Podfile:

post_install do |installer|
  installer.pods_project.targets.each do |target|
    flutter_additional_ios_build_settings(target)
    if target.name == 'okapi'
      target.build_configurations.each do |config|
        config.build_settings['SWIFT_ACTIVE_COMPILATION_CONDITIONS'] =
          '$(inherited) OKAPI_EXCLUDE_CONTACTS OKAPI_EXCLUDE_CALENDAR OKAPI_EXCLUDE_SPEECH'
      end
    end
  end
end

Flags: OKAPI_EXCLUDE_CAMERA, _MICROPHONE, _PHOTOS, _LOCATION, _NOTIFICATIONS, _CONTACTS, _CALENDAR, _SPEECH, _MEDIA_LIBRARY, _TRACKING, _SENSORS, _BLUETOOTH. Excluded permissions return notApplicable.

Google Play: SMS, call log, all-files access, background location, exact alarms and install-packages are restricted. Declare them only if your app qualifies, and complete Play Console's declarations.

Troubleshooting #

Symptom Fix
OkapiException(not_declared) Paste the lines from the message into the manifest / Info.plist, then do a full rebuild (not hot reload).
no_activity on Android Ask after the first frame, from a visible screen.
Always permanentlyDenied on iOS after one refusal That's iOS: it only ever shows the dialog once. Okapi guides the user to Settings.
storage is notApplicable on Android 13+ Use photos, videos or audio instead.
Background location never asked Ask locationWhenInUse first, or just ask locationAlways: Okapi orders it for you.
ATT dialog doesn't appear Make sure NSUserTrackingUsageDescription is set; Okapi already waits for the app to be active.
  • License: BSD 3-Clause. Free to use in any app, including commercial apps. Keep the copyright notice when you redistribute the code.
  • Name and logo: the BSD license covers the code only. A fork or modified copy must use a different name and logo and must not claim to be the official Okapi. See TRADEMARKS.md.
  • Terms: TERMS.md.
  • Disclaimer: Okapi is strictly a development package, not a decision-maker. It never decides which permissions an app requests, never accesses user data, and is not responsible for any permission requested or any data breach involving an app that uses it. Provided "as is", without any guarantee of results. See DISCLAIMER.md.
  • Privacy: Okapi collects nothing. See PRIVACY.md.
  • Security: see SECURITY.md. Contributing: see CONTRIBUTING.md.

Copyright © 2026 Sanket Santosh Vidya Manohar. "Okapi" and the Okapi logo are trademarks of Sanket Santosh Vidya Manohar.

Author #

Sanket Santosh Vidya Manohar


Okapi v1.0.0 · Made by Sanket Santosh Vidya Manohar

0
likes
160
points
--
downloads
screenshot

Documentation

Documentation
API reference

Publisher

verified publishersanketmanohar.com

Weekly Downloads

Complete permission manager for Flutter: guided flows, customisable UI, feature-based asking, OEM background guides, Doctor and Sync tools.

Repository (GitHub)
View/report issues
Contributing

Topics

#permissions #permission-handler #privacy #ui #onboarding

License

BSD-3-Clause (license)

Dependencies

flutter

More

Packages that depend on okapi

Packages that implement okapi