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.
import 'package:app_permissions/app_permissions.dart';
import 'package:app_storage/app_storage.dart';
import 'package:flutter/material.dart';
import 'snackbar.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize services
await StorageService.instance.init();
await PermissionService.instance.initialize();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'AppPermissions Demo',
theme: ThemeData(colorScheme: .fromSeed(seedColor: Colors.deepPurple)),
home: const SplashScreen(),
);
}
}
class SplashScreen extends StatefulWidget {
const SplashScreen({super.key});
@override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
@override
void initState() {
super.initState();
_checkFirstLaunch();
}
Future<void> _checkFirstLaunch() async {
await Future.delayed(const Duration(seconds: 1));
if (!mounted) return;
final launched = await StorageService.instance.get<bool>('launched');
if (!mounted) return;
if (launched == null || !launched) {
// First launch - show onboarding
await StorageService.instance.save('launched', true);
if (!mounted) return;
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const SafeOnboardingScreen()),
);
} else {
// Not first launch - go to home
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const HomeScreen()),
);
}
}
@override
Widget build(BuildContext context) {
return const Scaffold(body: Center(child: CircularProgressIndicator()));
}
}
class SafeOnboardingScreen extends StatefulWidget {
const SafeOnboardingScreen({super.key});
@override
State<SafeOnboardingScreen> createState() => _SafeOnboardingScreenState();
}
class _SafeOnboardingScreenState extends State<SafeOnboardingScreen> {
void _handleComplete() {
// Method call ensures we have access to current context
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const HomeScreen()),
);
}
}
@override
Widget build(BuildContext context) {
return PermissionOnboardingScreen(
groups: PermissionGroups.required,
onComplete: _handleComplete,
);
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(title: const Text("Permission Examples")),
body: ListView(
padding: const EdgeInsets.all(16.0),
children: [
// Example 1: Simple Permission Request
Card(
elevation: 2.0,
margin: const EdgeInsets.all(8.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
side: BorderSide.none,
),
child: ListTile(
leading: Icon(Icons.camera_alt, color: theme.colorScheme.primary),
title: const Text('Camera Permission'),
subtitle: const Text('Simple permission request'),
trailing: const Icon(Icons.chevron_right),
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const CameraExampleScreen()),
),
),
),
const SizedBox(height: 8.0),
// Example 2: Permission Guard
Card(
elevation: 2.0,
margin: const EdgeInsets.all(8.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
side: BorderSide.none,
),
child: ListTile(
leading: Icon(Icons.photo, color: theme.colorScheme.primary),
title: const Text('Permission Guard'),
subtitle: const Text('Auto-request with fallback'),
trailing: const Icon(Icons.chevron_right),
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const PhotoExampleScreen()),
),
),
),
const SizedBox(height: 8.0),
// Example 3: Permission Groups
Card(
elevation: 2.0,
margin: const EdgeInsets.all(8.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
side: BorderSide.none,
),
child: ListTile(
leading: Icon(Icons.group, color: theme.colorScheme.primary),
title: const Text('Permission Groups'),
subtitle: const Text('Request multiple permissions'),
trailing: const Icon(Icons.chevron_right),
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const GroupExampleScreen()),
),
),
),
const SizedBox(height: 8.0),
// Example 4: Permission Status
Card(
elevation: 2.0,
margin: const EdgeInsets.all(8.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
side: BorderSide.none,
),
child: ListTile(
leading: Icon(Icons.info, color: theme.colorScheme.primary),
title: const Text('Permission Status'),
subtitle: const Text('View all permission statuses'),
trailing: const Icon(Icons.chevron_right),
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const StatusExampleScreen()),
),
),
),
const SizedBox(height: 8.0),
// Example 5: Settings
Card(
elevation: 2.0,
margin: const EdgeInsets.all(8.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
side: BorderSide.none,
),
child: ListTile(
leading: Icon(Icons.settings, color: theme.colorScheme.primary),
title: const Text('Permission Settings'),
subtitle: const Text('Manage all permissions'),
trailing: const Icon(Icons.chevron_right),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const SettingsExampleScreen(),
),
),
),
),
],
),
);
}
}
// Example 1: Simple Camera Permission Request
class CameraExampleScreen extends StatelessWidget {
const CameraExampleScreen({super.key});
Future<void> _requestCamera(BuildContext context) async {
final result = await PermissionService.instance.requestWithRationale(
context,
const PermissionRequest(
permission: AppPermission.camera,
title: 'Camera Access Required',
message: 'We need camera access to take photos for your profile',
showRationale: true,
),
);
if (!context.mounted) return;
if (result.isGranted) {
AppSnackbar.show(
context,
message: 'Camera permission granted!',
type: AppSnackbarType.success,
);
} else if (result.isPermanentlyDenied) {
await PermissionService.instance.handlePermanentlyDenied(
context,
AppPermission.camera,
);
} else {
AppSnackbar.show(
context,
message: 'Camera permission denied',
type: AppSnackbarType.warning,
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Camera Permission')),
body: Center(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.camera_alt, size: 100),
const SizedBox(height: 32.0),
const Text(
'Camera Permission Example',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16.0),
const Text(
'This example shows how to request a single permission with a custom rationale dialog.',
textAlign: TextAlign.center,
),
const SizedBox(height: 32.0),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => _requestCamera(context),
child: const Text('Request Camera Permission'),
),
),
],
),
),
),
);
}
}
// Example 2: Permission Guard
class PhotoExampleScreen extends StatelessWidget {
const PhotoExampleScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Permission Guard')),
body: PermissionGuard(
permission: AppPermission.photos,
autoRequest: true,
rationale: 'Photo access is needed to select images for upload',
child: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.check_circle, size: 100, color: Colors.green),
SizedBox(height: 16.0),
Text(
'Photo Permission Granted!',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
SizedBox(height: 8.0),
Text('You can now select and upload photos.'),
],
),
),
),
);
}
}
// Example 3: Permission Groups
class GroupExampleScreen extends StatelessWidget {
const GroupExampleScreen({super.key});
Future<void> _requestMediaGroup(BuildContext context) async {
final results = await PermissionService.instance.requestGroup(
PermissionGroups.media,
);
if (!context.mounted) return;
final granted = results.entries
.where((e) => e.value.isGranted)
.map((e) => e.key);
final denied = results.entries
.where((e) => !e.value.isGranted)
.map((e) => e.key);
String message;
AppSnackbarType type;
if (denied.isEmpty) {
message = 'All media permissions granted!';
type = AppSnackbarType.success;
} else {
message =
'Granted: ${granted.length}, Denied: ${denied.length} permissions';
type = AppSnackbarType.warning;
}
AppSnackbar.show(context, message: message, type: type);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Permission Groups')),
body: ListView(
padding: const EdgeInsets.all(16.0),
children: [
const Padding(
padding: EdgeInsets.all(16.0),
child: Text(
'Permission groups allow you to request multiple related permissions at once.',
style: TextStyle(fontSize: 16),
),
),
...PermissionGroups.all.map(
(group) => Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: PermissionGroupCard(
group: group,
onToggle: () {
AppSnackbar.show(
context,
message: '${group.title} permissions updated',
type: AppSnackbarType.info,
);
},
),
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () => _requestMediaGroup(context),
child: const Icon(Icons.group_add),
),
);
}
}
// Example 4: Permission Status
class StatusExampleScreen extends StatelessWidget {
const StatusExampleScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Permission Status')),
body: ListView(
padding: const EdgeInsets.all(16.0),
children: [
const Padding(
padding: EdgeInsets.all(16.0),
child: Text(
'View the current status of all app permissions:',
style: TextStyle(fontSize: 16),
),
),
...AppPermission.values
.take(10)
.map(
(permission) => Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: PermissionStatusWidget(
permission: permission,
description: permission.defaultRationale,
showActionButton: true,
),
),
),
],
),
);
}
}
// Example 5: Settings
class SettingsExampleScreen extends StatelessWidget {
const SettingsExampleScreen({super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(title: const Text('Permission Settings')),
body: ListView(
padding: const EdgeInsets.all(16.0),
children: [
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.info, color: theme.colorScheme.primary),
const SizedBox(width: 8.0),
const Text(
'Permission Management',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 16.0),
const Text(
'You can manage all app permissions here. Some permissions '
'are required for the app to function properly.',
),
],
),
),
),
const SizedBox(height: 16.0),
Card(
child: ListTile(
leading: const Icon(Icons.settings_applications),
title: const Text('Open System Settings'),
subtitle: const Text('Manage permissions in device settings'),
trailing: const Icon(Icons.open_in_new),
onTap: () async {
await PermissionService.instance.openAppSettings();
},
),
),
const SizedBox(height: 16.0),
Card(
child: ListTile(
leading: const Icon(Icons.refresh),
title: const Text('Reset Permission States'),
subtitle: const Text('Clear cached permission data'),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
await PermissionService.instance.resetPermissionStates();
if (context.mounted) {
AppSnackbar.show(
context,
message: 'Permission states reset',
type: AppSnackbarType.success,
);
}
},
),
),
const SizedBox(height: 16.0),
Card(
child: ListTile(
leading: const Icon(Icons.info_outline),
title: const Text('View Request Counts'),
subtitle: const Text('See how many times each was requested'),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
final counts = await PermissionService.instance
.getRequestCounts();
if (context.mounted) {
showDialog(
context: context,
builder: (_) => AlertDialog(
title: const Text('Request Counts'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: counts.entries.map((e) {
return ListTile(
title: Text(e.key.displayName),
trailing: Text('${e.value}x'),
);
}).toList(),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Close'),
),
],
),
);
}
},
),
),
],
),
);
}
}