Feedback all'utente in Flutter: SnackBar, AlertDialog e ScaffoldMessenger

Foto di Zulfugar Karimov su Unsplash

GuidePrincipiante30 min Flutter 3.x

Feedback all'utente in Flutter: SnackBar, AlertDialog e ScaffoldMessenger

Parlare con l'utente: messaggi, conferme e avvisi

Un'app ben fatta non si limita a fare le cose: le comunica. Quando l'utente salva un dato, elimina un elemento o commette un errore, ha bisogno di un riscontro immediato.

Flutter mette a disposizione due strumenti fondamentali e velocissimi da usare:

  • SnackBar: un messaggio temporaneo che compare in basso, ideale per conferme non bloccanti ("Elemento salvato") e per offrire un'azione di annullamento.
  • AlertDialog: una finestra modale che blocca l'interazione finché l'utente non risponde, perfetta per confermare operazioni distruttive ("Vuoi davvero eliminare?").

In questa guida costruiremo una piccola schermata con una lista di note dove:

  1. mostreremo una SnackBar dopo un salvataggio;
  2. aggiungeremo un pulsante ANNULLA alla SnackBar;
  3. chiederemo conferma con un AlertDialog prima di eliminare;
  4. mostreremo un dialog di caricamento non chiudibile.

Prerequisiti: conoscere StatefulWidget, setState e la struttura base di uno Scaffold. Non serve alcun pacchetto esterno: usiamo solo il Material Design incluso in Flutter.

  1. 1

    Preparare la schermata di base con una lista di note

    Partiamo da uno StatefulWidget con una lista di stringhe in memoria. È la base su cui innesteremo i messaggi.

    Punti chiave:

    • la lista _note è lo stato della schermata: ogni modifica passa da setState;
    • il FloatingActionButton aggiunge una nota;
    • ogni ListTile ha un'icona per eliminare l'elemento.

    Sostituisci il contenuto di lib/main.dart con il codice qui sotto e lancia l'app: per ora non compare ancora nessun messaggio.

    import 'package:flutter/material.dart';
    
    void main() => runApp(const MyApp());
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Feedback utente',
          theme: ThemeData(
            colorSchemeSeed: Colors.indigo,
            useMaterial3: true,
          ),
          home: const NotePage(),
        );
      }
    }
    
    class NotePage extends StatefulWidget {
      const NotePage({super.key});
    
      @override
      State<NotePage> createState() => _NotePageState();
    }
    
    class _NotePageState extends State<NotePage> {
      final List<String> _note = <String>['Comprare il latte', 'Chiamare Luca'];
      int _contatore = 3;
    
      void _aggiungiNota() {
        setState(() {
          _note.add('Nota numero $_contatore');
          _contatore++;
        });
      }
    
      void _eliminaNota(int index) {
        setState(() {
          _note.removeAt(index);
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('Le mie note')),
          body: ListView.builder(
            itemCount: _note.length,
            itemBuilder: (context, index) {
              return ListTile(
                leading: const Icon(Icons.sticky_note_2_outlined),
                title: Text(_note[index]),
                trailing: IconButton(
                  icon: const Icon(Icons.delete_outline),
                  onPressed: () => _eliminaNota(index),
                ),
              );
            },
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: _aggiungiNota,
            child: const Icon(Icons.add),
          ),
        );
      }
    }

    Risultato atteso

    L'app mostra una lista di due note: il pulsante + ne aggiunge di nuove e l'icona cestino le rimuove, ma senza alcun messaggio di conferma.

  2. 2

    Mostrare una SnackBar con ScaffoldMessenger

    La SnackBar si mostra tramite ScaffoldMessenger.of(context).showSnackBar(...). ScaffoldMessenger è l'oggetto (introdotto in Flutter 2) che gestisce la coda dei messaggi: usarlo evita l'errore classico Scaffold.of() called with a context that does not contain a Scaffold.

    Alcune proprietà utili:

    • content: il widget da mostrare, di solito un Text;
    • duration: quanto resta a schermo (default 4 secondi);
    • behavior: SnackBarBehavior.floating: la fa "galleggiare" sopra il contenuto;
    • hideCurrentSnackBar(): chiude subito il messaggio in coda, utile se l'utente tocca il pulsante più volte di seguito.

    Aggiungiamo un metodo _mostraMessaggio e chiamiamolo quando si aggiunge una nota.

    void _mostraMessaggio(String testo) {
      final messenger = ScaffoldMessenger.of(context);
      messenger.hideCurrentSnackBar(); // evita l'accumulo di messaggi
      messenger.showSnackBar(
        SnackBar(
          content: Text(testo),
          duration: const Duration(seconds: 2),
          behavior: SnackBarBehavior.floating,
          margin: const EdgeInsets.all(16),
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(12),
          ),
        ),
      );
    }
    
    void _aggiungiNota() {
      setState(() {
        _note.add('Nota numero $_contatore');
        _contatore++;
      });
      _mostraMessaggio('Nota aggiunta con successo');
    }

    Risultato atteso

    Premendo il pulsante + compare in basso una SnackBar arrotondata con il testo "Nota aggiunta con successo", che scompare dopo 2 secondi.

  3. 3

    Aggiungere l'azione ANNULLA alla SnackBar

    La SnackBar accetta una SnackBarAction: un pulsante testuale a destra del messaggio. È il pattern standard per l'undo di un'eliminazione: si rimuove subito l'elemento e si dà all'utente qualche secondo per ripristinarlo.

    Modifichiamo _eliminaNota in modo che:

    1. salvi il testo e la posizione dell'elemento rimosso;
    2. mostri una SnackBar con l'azione ANNULLA;
    3. reinserisca la nota nella posizione originale se l'utente tocca il pulsante.

    Nota insert(index, nota): rimette l'elemento esattamente dov'era, non in fondo alla lista.

    void _eliminaNota(int index) {
      final notaRimossa = _note[index];
      final posizione = index;
    
      setState(() {
        _note.removeAt(index);
      });
    
      final messenger = ScaffoldMessenger.of(context);
      messenger.hideCurrentSnackBar();
      messenger.showSnackBar(
        SnackBar(
          content: Text('"$notaRimossa" eliminata'),
          duration: const Duration(seconds: 4),
          behavior: SnackBarBehavior.floating,
          margin: const EdgeInsets.all(16),
          action: SnackBarAction(
            label: 'ANNULLA',
            onPressed: () {
              setState(() {
                _note.insert(posizione, notaRimossa);
              });
            },
          ),
        ),
      );
    }

    Risultato atteso

    Eliminando una nota compare la SnackBar con il pulsante ANNULLA: toccandolo entro 4 secondi la nota torna nella stessa posizione della lista.

  4. 4

    Chiedere conferma con un AlertDialog

    Per le operazioni potenzialmente distruttive (come lo svuotamento della lista) serve una conferma esplicita. Si usa showDialog, che restituisce un Future con il valore passato a Navigator.pop(context, valore).

    Punti importanti:

    • il dialog si chiude con Navigator.pop(context, true/false);
    • showDialog<bool> è asincrono: si aspetta la risposta con await;
    • se l'utente tocca fuori dal dialog, il risultato è null: per questo usiamo confermato == true;
    • dopo un await il widget potrebbe non essere più a schermo: controlliamo if (!mounted) return; prima di usare di nuovo il context.

    Aggiungiamo un pulsante nell'AppBar che svuota la lista previa conferma.

    Future<bool> _chiediConferma() async {
      final confermato = await showDialog<bool>(
        context: context,
        builder: (dialogContext) {
          return AlertDialog(
            title: const Text('Eliminare tutte le note?'),
            content: const Text(
              'Questa operazione non può essere annullata.',
            ),
            actions: [
              TextButton(
                onPressed: () => Navigator.pop(dialogContext, false),
                child: const Text('Annulla'),
              ),
              FilledButton(
                onPressed: () => Navigator.pop(dialogContext, true),
                child: const Text('Elimina'),
              ),
            ],
          );
        },
      );
      return confermato ?? false; // tocco fuori dal dialog = annullato
    }
    
    Future<void> _svuotaLista() async {
      final conferma = await _chiediConferma();
      if (!mounted) return;
      if (conferma) {
        setState(() => _note.clear());
        _mostraMessaggio('Tutte le note sono state eliminate');
      }
    }
    
    // Nell'AppBar del metodo build:
    // AppBar(
    //   title: const Text('Le mie note'),
    //   actions: [
    //     IconButton(
    //       icon: const Icon(Icons.delete_sweep_outlined),
    //       onPressed: _svuotaLista,
    //     ),
    //   ],
    // )

    Risultato atteso

    Toccando l'icona nell'AppBar compare una finestra modale: con "Annulla" non succede nulla, con "Elimina" la lista si svuota e appare la SnackBar di conferma.

  5. 5

    Un dialog di caricamento non chiudibile

    Durante un'operazione lunga (una chiamata di rete, un salvataggio) può servire un dialog bloccante con uno spinner. Si ottiene con barrierDismissible: false e un PopScope che impedisce la chiusura con il tasto indietro di Android.

    Regola d'oro: chi apre il dialog deve chiuderlo. Qui lo apriamo prima dell'operazione e lo chiudiamo subito dopo con Navigator.pop, usando rootNavigator: true per essere certi di rimuovere la route del dialog.

    Simuliamo un salvataggio di 2 secondi che al termine mostra una SnackBar verde.

    void _mostraCaricamento() {
      showDialog<void>(
        context: context,
        barrierDismissible: false,
        builder: (_) => const PopScope(
          canPop: false,
          child: AlertDialog(
            content: Row(
              children: [
                CircularProgressIndicator(),
                SizedBox(width: 20),
                Expanded(child: Text('Salvataggio in corso...')),
              ],
            ),
          ),
        ),
      );
    }
    
    Future<void> _salvaSulServer() async {
      _mostraCaricamento();
    
      // Simulazione di un'operazione lunga
      await Future<void>.delayed(const Duration(seconds: 2));
    
      if (!mounted) return;
      Navigator.of(context, rootNavigator: true).pop(); // chiude il dialog
    
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: const Text('Note sincronizzate!'),
          backgroundColor: Colors.green.shade700,
          behavior: SnackBarBehavior.floating,
          margin: const EdgeInsets.all(16),
        ),
      );
    }
    
    // Nell'AppBar, accanto agli altri actions:
    // IconButton(
    //   icon: const Icon(Icons.cloud_upload_outlined),
    //   onPressed: _salvaSulServer,
    // ),

    Risultato atteso

    Toccando l'icona di upload appare per 2 secondi un dialog con spinner non chiudibile; poi si chiude da solo e compare una SnackBar verde "Note sincronizzate!".

  6. 6

    Ripulire il codice: un helper riutilizzabile

    Ripetere ScaffoldMessenger.of(context).showSnackBar(...) in ogni schermata è scomodo. Estraiamo la logica in una extension su BuildContext: da quel momento in tutta l'app basterà scrivere context.mostraSnack('Salvato!').

    Crea il file lib/utils/feedback_ui.dart e importalo dove serve. Con isErrore: true ottieni un messaggio in rosso, utile per gli errori.

    Buone pratiche finali:

    • SnackBar per informazioni non critiche, AlertDialog solo quando serve una decisione;
    • testi brevi e in una sola riga: la SnackBar non è il posto per uno stack trace;
    • una sola azione per SnackBar (di solito "Annulla");
    • controlla sempre mounted dopo un await prima di riusare il context.
    import 'package:flutter/material.dart';
    
    extension FeedbackUI on BuildContext {
      void mostraSnack(String messaggio, {bool isErrore = false}) {
        final messenger = ScaffoldMessenger.of(this);
        messenger.hideCurrentSnackBar();
        messenger.showSnackBar(
          SnackBar(
            content: Text(messaggio),
            behavior: SnackBarBehavior.floating,
            margin: const EdgeInsets.all(16),
            backgroundColor: isErrore ? Colors.red.shade700 : null,
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(12),
            ),
          ),
        );
      }
    
      Future<bool> chiediConferma({
        required String titolo,
        required String messaggio,
        String testoOk = 'Conferma',
        String testoAnnulla = 'Annulla',
      }) async {
        final risposta = await showDialog<bool>(
          context: this,
          builder: (ctx) => AlertDialog(
            title: Text(titolo),
            content: Text(messaggio),
            actions: [
              TextButton(
                onPressed: () => Navigator.pop(ctx, false),
                child: Text(testoAnnulla),
              ),
              FilledButton(
                onPressed: () => Navigator.pop(ctx, true),
                child: Text(testoOk),
              ),
            ],
          ),
        );
        return risposta ?? false;
      }
    }
    
    // Uso nella pagina:
    // context.mostraSnack('Nota aggiunta con successo');
    // final ok = await context.chiediConferma(
    //   titolo: 'Eliminare tutte le note?',
    //   messaggio: 'Questa operazione non può essere annullata.',
    //   testoOk: 'Elimina',
    // );

    Risultato atteso

    L'app funziona come prima, ma il codice delle schermate è più pulito: una riga per mostrare un messaggio e una per chiedere conferma, riutilizzabili ovunque.

CondividiXLinkedInFacebookWhatsApp

Commenti (0)

Ancora nessun commento. Inizia tu!