simple_permission_workflow
A small Dart/Flutter library that simplifies usage of the permission_handler inner library by offering a centralized permission workflow (status check, rationale dialog, request, open app settings).
Important: you must always declare the required permissions in the platform configuration files (for example
AndroidManifest.xmlfor Android,Info.plistfor iOS) before using helpers that read system data (contacts, location, etc.).
Features
- Single workflow to check and request permissions.
- Optional rationale UI support via
withRationale. - Injection of service factories to replace real permission services with fakes for tests.
- Returns a structured
SPWResponsedescribing the result.
Quick highlights
- Avoids direct calls to native
permission_handlercode in tests by allowing to inject fake services. - Designed to be small and testable.
Installation
Add the package to your pubspec.yaml (adjust source as required):
dependencies:
simple_permission_workflow: 0.0.9
Then run:
flutter pub get
Usage
Basic usage:
final spw = SimplePermissionWorkflow();
final response = await spw.launchWorkflow(SPWPermission.contacts);
if (response.granted) {
// permission granted
} else {
// handle denied or permanently denied
}
Using withRationale (optional): supply widgets to display rationale dialogs before requesting permissions.
final spw = SimplePermissionWorkflow().withRationale(
buildContext: context,
rationaleWidget: MyRationaleWidget(), // shown when rationale needed
permanentlyDeniedRationaleWidget: MyPermanentWidget(), // shown when permanently denied
openSettingsOnDismiss: true, // optional: open app settings after dismiss
);
final response = await spw.launchWorkflow(SPWPermission.location);
openSettingsOnDismiss (option):
- Type:
bool - Default:
false
The public parameter provided to withRationale is called openSettingsOnDismiss (internally the class uses the private field _openSettingsOnDismiss). When set to true, if the final status is permanentlyDenied or restricted (either from the initial status check or after the request), the library will first display the permanentlyDeniedRationaleWidget (if provided). After that dialog is dismissed (or immediately if no dialog is provided), the library will call openAppSettings() to open the app settings so the user can enable the permission manually.
Example enabling automatic opening of app settings after dismissing the permanently-denied rationale dialog:
final spw = SimplePermissionWorkflow().withRationale(
buildContext: context,
rationaleWidget: MyRationaleWidget(),
permanentlyDeniedRationaleWidget: MyPermanentWidget(),
openSettingsOnDismiss: true, // open settings after permanently-denied dialog dismiss
);
final response = await spw.launchWorkflow(SPWPermission.contacts);
Use this option thoughtfully: opening settings interrupts the app flow and may not be appropriate in all UX contexts (consider platform conventions and user expectations).
Service factory injection (recommended for testing):
final fakeResponse = SPWResponse()
..granted = true
..reason = 'granted';
final plugin = SimplePermissionWorkflow({
SPWPermission.contacts: () => FakeContactsService(fakeResponse),
});
final res = await plugin.launchWorkflow(SPWPermission.contacts);
FakeContactsService is any implementation of SPWPermissionService that returns the expected SPWResponse.
Contacts helper methods
The library exposes a typed way to access the concrete contacts permission service and helper methods that leverage the fast_contacts plugin for fast contact fetching and simple cleanup.
Example usage (explicitly shown):
final spw = SimplePermissionWorkflow();
// 1) Ensure permission is granted via workflow
final response = await spw.launchWorkflow(SPWPermission.contacts);
if (!response.granted) {
// handle denied / permanently denied
return;
}
// 2) Get the concrete contacts service instance (typed)
SPWContactsPermission perm =
spw.getServiceInstance<SPWContactsPermission>(SPWPermission.contacts);
// 3) Fetch contacts (uses fast_contacts internally)
List<Contact> fetchedContacts = await perm.retrieveContacts();
// 4) Order contacts by display name
List<Contact> orderedContacts = await perm.orderContacts(fetchedContacts);
// 5) Clean up contacts: remove empty names and contacts without phones
final nonEmptyNames = await perm.removeEmptyNames(orderedContacts);
final withPhones = await perm.removeEmptyPhoneNumbers(nonEmptyNames);
Notes:
retrieveContacts()returns aList<Contact>from thefast_contactspackage.orderContacts(...)returns a new list ordered bydisplayName(case-insensitive).removeEmptyNames(...)filters out contacts whosedisplayNameis empty or only whitespace.removeEmptyPhoneNumbers(...)filters out contacts that don't have at least one phone number.- Make sure your Android
AndroidManifest.xmland iOSInfo.plistcontain the required permission entries for reading contacts when using these helpers.
Supported permissions
The following permissions are exposed by the SPWPermission enum and handled by the library:
| Permission (enum) | Description | Main platforms |
|---|---|---|
accessMediaLocation |
Access to media location metadata (photos) | Android |
accessNotificationPolicy |
Access to notification policy settings (e.g. Do Not Disturb) | Android |
activityRecognition |
Physical activity recognition | Android |
appTrackingTransparency |
App Tracking Transparency (ATT) | iOS |
assistant |
Assistant permission (if applicable) | Android/iOS (platform dependent) |
audio |
Microphone / audio recording | Android/iOS |
backgroundRefresh |
Background app refresh (iOS background fetch / tasks) | iOS/Android |
bluetooth |
Bluetooth access | Android/iOS |
bluetoothAdvertise |
Bluetooth advertise (peripheral mode) | Android |
bluetoothConnect |
Bluetooth connect (to devices) | Android |
bluetoothScan |
Bluetooth scanning | Android |
calendar |
Calendar access (general) | Android/iOS |
calendarFullAccess |
Full access to calendar events | Android/iOS |
calendarWriteOnly |
Write-only calendar access | Android/iOS |
camera |
Camera access | Android/iOS |
contacts |
Access to device contacts | Android/iOS |
criticalAlerts |
Critical alerts permission (iOS) | iOS |
notifications |
Permission to send notifications | Android/iOS |
location |
Location (coarse/fine depending on platform) | Android/iOS |
photos |
Access to photos / gallery | Android/iOS |
API notes
-
SimplePermissionWorkflow([Map<SPWPermission, SPWPermissionService Function()>? factories])- By default the plugin registers real service factories (e.g.
SPWContactsPermission). Passing a map allows overriding any permission service with a factory returning a custom or fake implementation (useful for tests).
- By default the plugin registers real service factories (e.g.
-
Future<SPWResponse> launchWorkflow(SPWPermission permission)- Finds the factory for
permission, instantiates the service and runs itsrequestmethod. If no factory is found, it throwsArgumentError.
- Finds the factory for
-
SimplePermissionWorkflow.withRationale(...)supports an optionalopenSettingsOnDismissboolean parameter (defaultfalse). When true, the workflow will callopenAppSettings()after permanently denied / restricted status is shown and the permanently-denied rationale dialog (if any) is dismissed. For contacts flows that fetch or enumerate device contacts, prefer to callretrieveContacts()only afterlaunchWorkflowreturns granted to avoid platform exceptions.
Testing
To avoid MissingPluginException and binding errors in tests:
- Initialize Flutter bindings at top of your test
main():
TestWidgetsFlutterBinding.ensureInitialized();
- Inject fake services instead of using the platform MethodChannel implementations:
class FakeService implements SPWPermissionService {
final PermissionStatus status;
FakeService(this.status);
@override
Future<PermissionStatus> request() async => status;
}
final plugin = SimplePermissionWorkflow({
SPWPermission.contacts: () => FakeService(PermissionStatus.granted),
});
- To test a
Futurethat should throw, use the forms below (don't directly await a Future expected to throw):
expect(plugin.launchWorkflow(SPWPermission.location), throwsArgumentError);
// or
await expectLater(plugin.launchWorkflow(SPWPermission.location), throwsArgumentError);
- Compare fields of
SPWResponse(e.g.res.granted) rather than instance identity unless==is implemented.
Run tests:
flutter test
Development notes
- To add a new permission type: implement an
SPWPermissionServiceinlib/services/impl/and register its factory or override via constructor injection. - Keep UI rationale widgets out of core logic;
withRationaleonly holds references and triggers dialogs when a validBuildContextis available.
Contributing
See CONTRIBUTING.md for contribution guidelines and PR process.
Changelog
See CHANGELOG.md for recent changes. (0.0.8 includes additional permissions added to SPWPermission.)
License
Apache-2.0 — see LICENSE for the full text.
Libraries
- core/spw_check_status_response
- core/spw_permission
- core/spw_response
- services/impl/activity_recognition_permission_service
- services/impl/app_tracking_transparency_permission_service
- services/impl/assistant_permission_service
- services/impl/audio_permission_service
- services/impl/background_refresh_permission_service
- services/impl/bluetooth_advertise_permission_service
- services/impl/bluetooth_connect_permission_service
- services/impl/bluetooth_permission_service
- services/impl/bluetooth_scan_permission_service
- services/impl/calendar_full_access_permission_service
- services/impl/calendar_write_only_permission_service
- services/impl/camera_permission_service
- services/impl/contacts_permission_service
- services/impl/critical_alerts_permission_service
- services/impl/location_permission_service
- services/impl/media_location_permission_service
- services/impl/notification_policy_permission_service
- services/impl/notifications_permission_service
- services/impl/photos_permission_service
- services/permission_service
- simple_permission_workflow
- simple_permission_workflow_method_channel
- simple_permission_workflow_platform_interface