baaba_permission_handler 1.0.0
baaba_permission_handler: ^1.0.0 copied to clipboard
Complete permission flow for Android and iOS: request, rationale, denied and permanently denied handling, Settings guidance dialogs. Fully customizable.
example/lib/main.dart
import 'package:baaba_permission_handler/baaba_permission_handler.dart';
import 'package:flutter/material.dart';
import 'brand_dialog.dart';
/// What the demo lets you switch at runtime.
enum DemoStyle { packageDefault, customBrand }
enum DemoTheme { system, green, pink }
final style = ValueNotifier(DemoStyle.packageDefault);
final demoTheme = ValueNotifier(DemoTheme.system);
final urdu = ValueNotifier(false);
/// Rebuilt whenever a switch changes: the config is plain data, so an app
/// can swap wording, language and dialog design at any time.
PermissionConfig buildConfig() => PermissionConfig(
appName: urdu.value ? 'پبلک سیفٹی' : 'Baaba',
strings: urdu.value ? urduStrings : const PermissionStrings(),
texts: urdu.value
? urduTexts
: const {
AppPermission.camera: PermissionTexts(
rationale: 'Scan your CNIC and take profile photos.',
),
},
dialogBuilder: style.value == DemoStyle.customBrand
? brandPermissionDialog
: null,
// Only the permission dialogs turn right-to-left; the rest of the app
// keeps its own direction.
textDirection: urdu.value ? TextDirection.rtl : TextDirection.ltr,
observer: _PrintObserver(),
);
void main() {
BaabaPermissions.init(buildConfig());
for (final n in [style, demoTheme, urdu]) {
n.addListener(() => BaabaPermissions.instance.config = buildConfig());
}
runApp(const ExampleApp());
}
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) => ListenableBuilder(
listenable: Listenable.merge([demoTheme, urdu]),
builder: (context, _) {
final fixed = switch (demoTheme.value) {
DemoTheme.green => greenTheme,
DemoTheme.pink => pinkTheme,
DemoTheme.system => null,
};
return MaterialApp(
title: 'Permissions',
theme:
fixed ??
ThemeData(colorSchemeSeed: Colors.teal, useMaterial3: true),
darkTheme:
fixed ??
ThemeData(
colorSchemeSeed: Colors.teal,
brightness: Brightness.dark,
useMaterial3: true,
),
home: const PermissionsPage(),
);
},
);
}
class PermissionsPage extends StatefulWidget {
const PermissionsPage({super.key});
@override
State<PermissionsPage> createState() => _PermissionsPageState();
}
class _PermissionsPageState extends State<PermissionsPage> {
static const _shown = [
AppPermission.camera,
AppPermission.microphone,
AppPermission.photos,
AppPermission.location,
AppPermission.locationAlways,
AppPermission.notification,
AppPermission.contacts,
AppPermission.calendar,
AppPermission.bluetooth,
AppPermission.activityRecognition,
AppPermission.speech,
AppPermission.appTrackingTransparency,
AppPermission.systemAlertWindow,
AppPermission.scheduleExactAlarm,
AppPermission.ignoreBatteryOptimizations,
];
final _permissions = BaabaPermissions.instance;
Map<AppPermission, PermissionResult> _results = {};
PermissionPresentation _presentation = PermissionPresentation.dialog;
RationaleMode _rationale = RationaleMode.afterDenial;
late final AppLifecycleListener _lifecycle;
@override
void initState() {
super.initState();
_lifecycle = AppLifecycleListener(onResume: _refresh);
_refresh();
}
@override
void dispose() {
_lifecycle.dispose();
super.dispose();
}
Future<void> _refresh() async {
final results = await _permissions.checkMany(_shown);
if (mounted) setState(() => _results = results.results);
}
PermissionRequestOptions get _options => PermissionRequestOptions(
presentation: _presentation,
rationaleMode: _rationale,
);
Future<void> _request(AppPermission p) async {
final r = await p.request(options: _options);
if (!mounted) return;
setState(() => _results[p] = r);
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('${p.name}: ${r.state.name}')));
}
Future<void> _requestVideoCall() async {
final r = await [
AppPermission.camera,
AppPermission.microphone,
].request(options: _options);
if (!mounted) return;
setState(() => _results.addAll(r.results));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
r.allAllowed ? 'Ready for a video call' : 'Missing: ${r.notAllowed}',
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Permissions'),
actions: [
IconButton(
tooltip: 'Open Settings',
icon: const Icon(Icons.settings_outlined),
onPressed: _permissions.openAppSettings,
),
IconButton(
tooltip: 'Refresh',
icon: const Icon(Icons.refresh),
onPressed: _refresh,
),
],
),
body: ListView(
padding: const EdgeInsets.only(bottom: 32),
children: [
_Controls(
presentation: _presentation,
rationale: _rationale,
onPresentation: (v) => setState(() => _presentation = v),
onRationale: (v) => setState(() => _rationale = v),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Wrap(
spacing: 8,
runSpacing: 8,
children: [
FilledButton.tonalIcon(
onPressed: _requestVideoCall,
icon: const Icon(Icons.video_call_outlined),
label: const Text('Camera + mic together'),
),
OutlinedButton.icon(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute<void>(builder: (_) => const _GatePage()),
),
icon: const Icon(Icons.lock_outline),
label: const Text('PermissionGate demo'),
),
],
),
),
const SizedBox(height: 8),
for (final p in _shown)
_PermissionTile(
permission: p,
result: _results[p],
onTap: () => _request(p),
),
],
),
);
}
}
class _Controls extends StatelessWidget {
final PermissionPresentation presentation;
final RationaleMode rationale;
final ValueChanged<PermissionPresentation> onPresentation;
final ValueChanged<RationaleMode> onRationale;
const _Controls({
required this.presentation,
required this.rationale,
required this.onPresentation,
required this.onRationale,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Show dialogs as',
style: Theme.of(context).textTheme.labelLarge,
),
const SizedBox(height: 8),
SegmentedButton<PermissionPresentation>(
segments: const [
ButtonSegment(
value: PermissionPresentation.dialog,
label: Text('Dialog'),
),
ButtonSegment(
value: PermissionPresentation.bottomSheet,
label: Text('Sheet'),
),
ButtonSegment(
value: PermissionPresentation.snackBar,
label: Text('Snack bar'),
),
],
selected: {presentation},
onSelectionChanged: (s) => onPresentation(s.single),
),
const SizedBox(height: 16),
Text('Rationale', style: Theme.of(context).textTheme.labelLarge),
DropdownButton<RationaleMode>(
value: rationale,
isExpanded: true,
items: [
for (final m in RationaleMode.values)
DropdownMenuItem(value: m, child: Text(m.name)),
],
onChanged: (m) => m == null ? null : onRationale(m),
),
const SizedBox(height: 8),
const _DemoSwitches(),
],
),
);
}
}
class _DemoSwitches extends StatelessWidget {
const _DemoSwitches();
@override
Widget build(BuildContext context) => ListenableBuilder(
listenable: Listenable.merge([style, demoTheme, urdu]),
builder: (context, _) => Wrap(
spacing: 8,
runSpacing: 4,
children: [
ChoiceChip(
label: const Text('Default dialog'),
selected: style.value == DemoStyle.packageDefault,
onSelected: (_) => style.value = DemoStyle.packageDefault,
),
ChoiceChip(
label: const Text('Custom dialog'),
selected: style.value == DemoStyle.customBrand,
onSelected: (_) => style.value = DemoStyle.customBrand,
),
for (final t in DemoTheme.values)
ChoiceChip(
label: Text('Theme: ${t.name}'),
selected: demoTheme.value == t,
onSelected: (_) => demoTheme.value = t,
),
FilterChip(
label: const Text('اردو'),
selected: urdu.value,
onSelected: (v) => urdu.value = v,
),
],
),
);
}
class _PermissionTile extends StatelessWidget {
final AppPermission permission;
final PermissionResult? result;
final VoidCallback onTap;
const _PermissionTile({
required this.permission,
required this.result,
required this.onTap,
});
@override
Widget build(BuildContext context) {
final texts = defaultPermissionTexts(permission);
final state = result?.state;
final scheme = Theme.of(context).colorScheme;
final color = switch (state) {
null => scheme.outline,
_ when state.isAllowed => Colors.green,
PermissionState.denied => Colors.orange,
_ => scheme.error,
};
return ListTile(
leading: Icon(texts.icon),
title: Text(texts.name ?? permission.name),
subtitle: Text(
result?.reason ?? texts.rationale ?? '',
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
trailing: Chip(
label: Text(state?.name ?? '…'),
side: BorderSide(color: color),
labelStyle: TextStyle(color: color),
),
onTap: onTap,
);
}
}
class _GatePage extends StatelessWidget {
const _GatePage();
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Scanner')),
body: PermissionGate(
permission: AppPermission.camera,
child: const Center(child: Icon(Icons.qr_code_scanner, size: 160)),
),
);
}
class _PrintObserver extends PermissionObserver {
@override
void onResult(PermissionResult result) => debugPrint('permission: $result');
@override
void onError(Object error, StackTrace stackTrace, String where) =>
debugPrint('permission error in $where: $error');
}