baaba_permission_handler 1.1.0 copy "baaba_permission_handler: ^1.1.0" to clipboard
baaba_permission_handler: ^1.1.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');
}
0
likes
160
points
422
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Complete permission flow for Android and iOS: request, rationale, denied and permanently denied handling, Settings guidance dialogs. Fully customizable.

Repository (GitHub)
View/report issues

Topics

#permissions #permission-handler #android #ios #dialog

License

MIT (license)

Dependencies

app_settings, device_info_plus, flutter, permission_handler

More

Packages that depend on baaba_permission_handler