Centralized permission management system with automatic requests, status tracking, rationale dialogs, settings navigation, and graceful degradation.
Features
- Centralized Permission Requests - Single source of truth for all permission handling
- Status Tracking - Track granted, denied, and permanently denied permissions
- Rationale Dialogs - Explain why permissions are needed before requesting
- Settings Navigation - Automatic navigation to app settings for permanently denied permissions
- Batch Requests - Request multiple permissions at once
- Permission Groups - Pre-defined groups for common features (media, storage, etc.)
- Platform-Specific Handling - Automatic iOS vs Android differences
- Permission Caching - Avoid redundant checks with smart caching
- Permission Listeners - React to permission changes in real-time
- Graceful Degradation - App works even without optional permissions
- Educational UI - Show permission benefits before requesting
- Analytics Ready - Built-in logging for permission grant/denial tracking
Please see permission_handler for setup instructions of permissions on a specific platform.
Installation
Add to your pubspec.yaml:
dependencies:
app_permissions: ^1.0.0
Then run:
flutter pub get
Quick Start
1. Initialize Permission Service
import 'package:app_permissions/app_permissions.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize permission service
await PermissionService.instance.initialize();
runApp(MyApp());
}
2. Request a Permission
// Simple request
final result = await PermissionService.instance.requestPermission(
AppPermission.camera,
);
if (result.isGranted) {
// Permission granted - proceed
openCamera();
} else if (result.isPermanentlyDenied) {
// Show settings dialog
await PermissionService.instance.handlePermanentlyDenied(
context,
AppPermission.camera,
);
}
3. Use Permission Guard
// Automatically handle permissions for a widget
PermissionGuard(
permission: AppPermission.camera,
autoRequest: true,
rationale: 'Camera needed for profile photo',
child: CameraWidget(),
fallback: Text('Camera not available'),
)
Available Permissions
enum AppPermission {
// Notifications
notifications,
// Storage & Files
storage,
photos,
videos,
// Media
camera,
microphone,
// Location
location,
locationAlways,
locationWhenInUse,
// Communication
contacts,
phone,
// Calendar
calendar,
// Sensors
sensors,
activityRecognition,
// Bluetooth
bluetooth,
bluetoothScan,
bluetoothConnect,
}
Permission Groups
Pre-defined permission groups for common features:
// Media permissions (camera + microphone + photos)
final results = await PermissionService.instance.requestGroup(
PermissionGroups.media,
);
// Available groups:
PermissionGroups.media
PermissionGroups.storage // Required
PermissionGroups.notifications
PermissionGroups.location
PermissionGroups.communication
PermissionGroups.calendar
PermissionGroups.bluetooth
// Get all groups
PermissionGroups.all
// Get only required groups
PermissionGroups.required
Core API
PermissionService
final service = PermissionService.instance;
// Check permission status (no request)
final status = await service.checkPermission(AppPermission.camera);
// Check multiple permissions
final statuses = await service.checkPermissions([
AppPermission.camera,
AppPermission.microphone,
]);
// Request permission
final result = await service.requestPermission(AppPermission.camera);
// Request with custom rationale
final result = await service.requestWithRationale(
context,
PermissionRequest(
permission: AppPermission.camera,
title: 'Camera Access Required',
message: 'We need camera access to take photos for assignments',
showRationale: true,
),
);
// Request multiple permissions
final results = await service.requestPermissions([
AppPermission.camera,
AppPermission.microphone,
]);
// Request permission group
final results = await service.requestGroup(
PermissionGroups.media,
context: context,
);
// Open app settings
await service.openAppSettings();
// Handle permanently denied
await service.handlePermanentlyDenied(
context,
AppPermission.camera,
);
// Get permission state
final state = service.getPermissionState(AppPermission.camera);
// Watch permission changes
service.watchPermission(AppPermission.camera).listen((state) {
print('Camera permission: ${state.status}');
});
// Check if all permissions granted
final hasAll = await service.hasAllPermissions([
AppPermission.camera,
AppPermission.microphone,
]);
// Get missing permissions
final missing = await service.getMissingPermissions([
AppPermission.camera,
AppPermission.microphone,
]);
Widgets
PermissionGuard
Wraps widgets that require permissions:
PermissionGuard(
permission: AppPermission.camera,
autoRequest: true,
rationale: 'Camera needed for profile photo',
child: CameraWidget(),
fallback: Text('Camera not available'),
onPermissionGranted: () => print('Granted!'),
onPermissionDenied: () => print('Denied!'),
)
PermissionStatusWidget
Shows current permission status:
PermissionStatusWidget(
permission: AppPermission.camera,
title: 'Camera',
description: 'Take photos for assignments',
showActionButton: true,
)
PermissionRequestDialog
Shows rationale before requesting:
showDialog(
context: context,
builder: (_) => PermissionRequestDialog(
permission: AppPermission.camera,
title: 'Camera Access',
message: 'We need camera access for assignments',
onAllow: () async {
Navigator.pop(context);
final result = await PermissionService.instance.requestPermission(
AppPermission.camera,
);
if (result.isGranted) {
openCamera();
}
},
onDeny: () => Navigator.pop(context),
),
);
PermissionSettingsDialog
Shows when permission is permanently denied:
showDialog(
context: context,
builder: (_) => PermissionSettingsDialog(
permission: AppPermission.camera,
title: 'Camera Permission Denied',
message: 'Please enable camera in Settings',
),
);
PermissionOnboardingScreen
Full-screen onboarding for permission groups:
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => PermissionOnboardingScreen(
groups: PermissionGroups.all,
onComplete: () {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => HomeScreen()),
);
},
),
),
);
PermissionGroupCard
Shows permission group with toggle:
PermissionGroupCard(
group: PermissionGroups.media,
onToggle: () async {
await PermissionService.instance.requestGroup(
PermissionGroups.media,
);
},
)
PermissionRequiredOverlay
Blocks UI until permission granted:
PermissionRequiredOverlay(
requiredPermissions: [AppPermission.storage],
title: 'Storage Permission Required',
message: 'Storage access is required for this app to work',
child: MainAppContent(),
)
Extensions
Context Extensions
// Quick permission check
final hasCamera = await context.hasPermission(AppPermission.camera);
// Quick permission request
final granted = await context.requestPermission(
AppPermission.camera,
rationale: 'Camera needed for photos',
);
AppPermission Extensions
final permission = AppPermission.camera;
// User-friendly name
print(permission.displayName); // "Camera"
// Icon
Icon(permission.icon)
// Default rationale
print(permission.defaultRationale);
// Detailed description
print(permission.detailedDescription);
// Is required?
if (permission.isRequired) {
// This permission is mandatory
}
PermissionStatus Extensions
final status = PermissionStatus.granted;
// Status checks
status.isGranted
status.isDenied
status.isPermanentlyDenied
status.canRequest
status.shouldNavigateToSettings
// Display text
print(status.displayText); // "Allowed"
// Color
print(status.colorHex); // "#4CAF50"
Usage Examples
Example 1: Download File
Future<void> downloadFile(String url) async {
// Check if we have permission
final hasPermission = await context.hasPermission(AppPermission.storage);
if (!hasPermission) {
// Request permission with rationale
final granted = await context.requestPermission(
AppPermission.storage,
rationale: 'Storage permission needed to save the file',
);
if (!granted) {
AppSnackbar.show(
context,
message: 'Cannot download without storage permission',
type: AppSnackbarType.error,
);
return;
}
}
// Proceed with download
await downloadService.download(url);
}
Example 2: Video Assignment
Future<void> recordVideoAssignment() async {
// Request all media permissions
final results = await PermissionService.instance.requestGroup(
PermissionGroups.media,
);
final allGranted = results.values.every((r) => r.isGranted);
if (allGranted) {
// Open video recorder
Navigator.push(
context,
MaterialPageRoute(builder: (_) => VideoRecorderScreen()),
);
} else {
// Show which permissions are missing
final denied = results.entries
.where((e) => !e.value.isGranted)
.map((e) => e.key.displayName)
.join(', ');
AppSnackbar.show(
context,
message: 'Required permissions: $denied',
type: AppSnackbarType.warning,
);
}
}
Example 3: Profile Photo
class ProfilePhotoScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Profile Photo')),
body: PermissionGuard(
permission: AppPermission.camera,
autoRequest: true,
rationale: 'Camera access needed for profile photo',
child: CameraPreview(),
fallback: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.camera_alt, size: 64, color: Colors.grey),
const SizedBox(height: 16),
const Text('Camera not available'),
const SizedBox(height: 16),
AppButton(
label: 'Open Settings',
onPressed: () async {
await PermissionService.instance.openAppSettings();
},
),
],
),
),
),
);
}
}
Example 4: First Launch Onboarding
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: FutureBuilder<bool>(
future: _isFirstLaunch(),
builder: (context, snapshot) {
if (snapshot.data == true) {
// First launch - show onboarding
return PermissionOnboardingScreen(
groups: PermissionGroups.required,
onComplete: () {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => HomeScreen()),
);
},
);
}
return HomeScreen();
},
),
);
}
Future<bool> _isFirstLaunch() async {
final launched = await StorageService.instance.get<bool>('launched');
if (launched == null) {
await StorageService.instance.save('launched', true);
return true;
}
return false;
}
}
Example 5: Listen to Permission Changes
class FeatureScreen extends StatefulWidget {
@override
_FeatureScreenState createState() => _FeatureScreenState();
}
class _FeatureScreenState extends State<FeatureScreen>
with WidgetsBindingObserver {
bool _hasPermission = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_checkPermission();
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
// App came back from background - check if permission changed
_checkPermission();
}
}
Future<void> _checkPermission() async {
final status = await PermissionService.instance.checkPermission(
AppPermission.camera,
);
setState(() {
_hasPermission = status.isGranted;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: _hasPermission
? CameraWidget()
: const Center(child: Text('Camera permission required')),
);
}
}
Integration with Other Packages
app_notifications
// OLD (before app_permissions):
await FirebaseMessaging.instance.requestPermission();
// NEW (with app_permissions):
import 'package:app_permissions/app_permissions.dart';
final granted = await PermissionService.instance.requestPermission(
AppPermission.notifications,
);
if (granted.isGranted) {
await FirebaseMessaging.instance.requestPermission();
}
media_downloader
// Before downloading
final hasPermission = await PermissionService.instance.requestPermission(
AppPermission.storage,
rationale: 'Storage access needed to download files',
);
if (!hasPermission.isGranted) {
throw StoragePermissionDeniedException();
}
// Proceed with download
await mediaDownloader.download(url);
media_player
// Before accessing camera
PermissionGuard(
permission: AppPermission.camera,
autoRequest: true,
child: CameraView(),
)
Best Practices
1. Request Just-In-Time
Don't request all permissions at app launch. Request when the feature is first used:
// ❌ BAD - Request everything at launch
void main() async {
await PermissionService.instance.requestPermissions([
AppPermission.camera,
AppPermission.microphone,
AppPermission.location,
]);
runApp(MyApp());
}
// ✅ GOOD - Request when needed
void onTakePhotoPressed() async {
final granted = await PermissionService.instance.requestPermission(
AppPermission.camera,
);
if (granted.isGranted) {
openCamera();
}
}
2. Always Show Rationale
Explain why you need the permission before requesting:
// ❌ BAD - No context
await PermissionService.instance.requestPermission(AppPermission.camera);
// ✅ GOOD - Clear rationale
await PermissionService.instance.requestWithRationale(
context,
PermissionRequest(
permission: AppPermission.camera,
title: 'Camera Access Required',
message: 'We need camera access to let you take photos for assignments',
showRationale: true,
),
);
3. Handle Permanently Denied
Always provide a path to settings for permanently denied permissions:
final result = await PermissionService.instance.requestPermission(
AppPermission.camera,
);
if (result.isPermanentlyDenied) {
// Show settings dialog
await PermissionService.instance.handlePermanentlyDenied(
context,
AppPermission.camera,
);
}
4. Graceful Degradation
App should work even without optional permissions:
// Check permission first
final hasLocation = await context.hasPermission(AppPermission.location);
if (hasLocation) {
// Show map with user location
showMapWithLocation();
} else {
// Show map without user location (fallback)
showMapWithoutLocation();
}
5. Use Permission Groups
Group related permissions together:
// ❌ BAD - Request individually
await PermissionService.instance.requestPermission(AppPermission.camera);
await PermissionService.instance.requestPermission(AppPermission.microphone);
await PermissionService.instance.requestPermission(AppPermission.photos);
// ✅ GOOD - Use permission group
await PermissionService.instance.requestGroup(
PermissionGroups.media,
);
Platform-Specific Notes
iOS
- Provisional Notifications: iOS can grant provisional notification permissions
- Limited Photo Access: iOS 14+ can grant limited photo library access
- Location Always: Requires two-step permission flow
- Info.plist: Add usage descriptions for all permissions
Android
- Runtime Permissions: Android 6+ requires runtime permission requests
- shouldShowRationale: Android provides API to check if rationale should be shown
- Storage Changes: Android 10+ uses Scoped Storage
- AndroidManifest.xml: Declare all permissions needed
Testing
import 'package:flutter_test/flutter_test.dart';
import 'package:app_permissions/app_permissions.dart';
void main() {
setUpAll(() async {
await PermissionService.instance.initialize();
});
tearDown(() async {
await PermissionService.instance.resetPermissionStates();
});
test('check permission returns status', () async {
final status = await PermissionService.instance.checkPermission(
AppPermission.camera,
);
expect(status, isA<PermissionStatus>());
});
test('permission state is cached', () {
final state = PermissionService.instance.getPermissionState(
AppPermission.camera,
);
expect(state, isA<PermissionState>());
});
}
Troubleshooting
Permission Always Returns Denied
- Check platform-specific permission declarations (Info.plist / AndroidManifest.xml)
- Ensure permission is not restricted by device policy
- Check if permission was permanently denied previously
Permission Dialog Not Showing
- Ensure you're not requesting same permission repeatedly
- Check if permission was already granted
- Verify app has required permission declarations
Settings Dialog Not Opening
- Check app_settings package is properly installed
- Test on real device (not all simulators support opening settings)
- Verify platform-specific implementation exists
License
MIT License - See LICENSE file for details
Contributing
- Fork the repository
- Create your feature branch
- Add tests for new features
- Submit a pull request
Support
For issues and feature requests, please create an issue on the repository.
Libraries
- app_permissions
- Centralized permission management system.