Perché serve gestire i permessi

Le app moderne accedono a risorse sensibili: fotocamera, microfono, posizione, contatti, archiviazione. Sia Android che iOS richiedono che l'utente conceda esplicitamente questi accessi a runtime. Gestire male questo flusso porta a crash, recensioni negative e funzionalità bloccate.

In Flutter lo standard di fatto è il pacchetto permission_handler, che astrae le differenze tra piattaforme con un'API uniforme.

Installazione

Aggiungi la dipendenza:

dependencies:
  permission_handler: ^11.3.1

Configurazione Android

Dichiara i permessi nel file android/app/src/main/AndroidManifest.xml:

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

Da Android 13 (API 33) le notifiche richiedono il permesso runtime POST_NOTIFICATIONS.

Configurazione iOS

Su iOS devi aggiungere le stringhe di motivazione (purpose strings) in ios/Runner/Info.plist. Senza queste l'app verrà rifiutata da App Store:

<key>NSCameraUsageDescription</key>
<string>L'app usa la fotocamera per scattare foto del profilo.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>La posizione serve per mostrarti i contenuti vicini.</string>

Inoltre permission_handler su iOS richiede di abilitare i permessi tramite macro nel Podfile, decommentando le voci pertinenti nella sezione post_install:

post_install do |installer|
  installer.pods_project.targets.each do |target|
    flutter_additional_ios_build_settings(target)
    target.build_configurations.each do |config|
      config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
        '$(inherited)',
        'PERMISSION_CAMERA=1',
        'PERMISSION_LOCATION=1',
      ]
    end
  end
end

Richiedere un permesso

L'API base ruota attorno all'enum Permission e alla classe PermissionStatus:

import 'package:permission_handler/permission_handler.dart';

Future<bool> richiediFotocamera() async {
  final status = await Permission.camera.request();
  return status.isGranted;
}

Il metodo request() mostra il dialog di sistema solo se necessario; se il permesso è già concesso ritorna immediatamente lo stato.

Gli stati possibili

PermissionStatus può assumere diversi valori, e gestirli tutti è la chiave per un flusso robusto:

  • granted: permesso concesso.
  • denied: negato, ma è ancora possibile richiederlo di nuovo.
  • permanentlyDenied: negato in modo permanente (l'utente ha scelto "Non chiedere più"). Va gestito aprendo le impostazioni.
  • restricted: bloccato a livello di sistema (es. controllo parentale, solo iOS).
  • limited: accesso parziale (es. solo alcune foto, iOS 14+).
Future<void> gestisciPosizione(BuildContext context) async {
  final status = await Permission.locationWhenInUse.request();

  switch (status) {
    case PermissionStatus.granted:
    case PermissionStatus.limited:
      // Procedi con la funzionalità
      break;
    case PermissionStatus.denied:
      _mostraMessaggio(context, 'Permesso necessario per continuare.');
      break;
    case PermissionStatus.permanentlyDenied:
      _mostraDialogImpostazioni(context);
      break;
    case PermissionStatus.restricted:
      _mostraMessaggio(context, 'Accesso bloccato dal sistema.');
      break;
    default:
      break;
  }
}

Aprire le impostazioni dell'app

Quando un permesso è permanentlyDenied l'unica via è invitare l'utente ad abilitarlo manualmente:

void _mostraDialogImpostazioni(BuildContext context) {
  showDialog(
    context: context,
    builder: (_) => AlertDialog(
      title: const Text('Permesso richiesto'),
      content: const Text(
        'Abilita il permesso dalle impostazioni per usare questa funzione.',
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('Annulla'),
        ),
        TextButton(
          onPressed: () {
            openAppSettings();
            Navigator.pop(context);
          },
          child: const Text('Apri impostazioni'),
        ),
      ],
    ),
  );
}

La funzione globale openAppSettings() apre la schermata di sistema dell'app.

Controllare lo stato senza richiedere

A volte vuoi solo sapere se un permesso è concesso, senza mostrare il dialog (per esempio all'avvio per decidere cosa visualizzare):

final status = await Permission.notification.status;
if (status.isGranted) {
  // Mostra contenuti che richiedono il permesso
}

Richiedere più permessi insieme

È possibile richiedere un gruppo di permessi in un'unica chiamata:

Map<Permission, PermissionStatus> esiti = await [
  Permission.camera,
  Permission.microphone,
].request();

if (esiti[Permission.camera]!.isGranted &&
    esiti[Permission.microphone]!.isGranted) {
  // Avvia registrazione video
}

Best practice

  • Chiedi al momento giusto: richiedi un permesso solo quando l'utente attiva la funzionalità che lo necessita, non all'avvio. Aumenta il tasso di concessione.
  • Spiega il perché: mostra una schermata o un dialog "di pre-permesso" che spiega il motivo prima di lanciare il dialog di sistema.
  • Gestisci sempre il rifiuto permanente: non lasciare l'utente in un vicolo cieco, offri il link alle impostazioni.
  • Ricontrolla al ritorno in foreground: l'utente può revocare i permessi dalle impostazioni mentre l'app è in background. Usa WidgetsBindingObserver per riverificare lo stato.
  • Centralizza la logica: incapsula la gestione dei permessi in un service dedicato, per evitare codice duplicato e facilitare i test.
class PermessiService {
  Future<bool> assicuraFotocamera() async {
    final status = await Permission.camera.status;
    if (status.isGranted) return true;
    if (status.isPermanentlyDenied) return false;
    return (await Permission.camera.request()).isGranted;
  }
}

Conclusione

Una gestione attenta dei permessi non è solo un requisito tecnico, ma un fattore di fiducia per l'utente. Con permission_handler hai un'API coerente tra Android e iOS: il resto dipende dal contesto e dalla chiarezza con cui comunichi le tue richieste. Centralizza la logica, gestisci tutti gli stati e chiedi sempre al momento opportuno.