app_permissions 1.0.0
app_permissions: ^1.0.0 copied to clipboard
Centralized permission management system with automatic requests, status tracking, rationale dialogs, and graceful degradation.
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.