Menu a tendina in Flutter: DropdownButton, DropdownButtonFormField e DropdownMenu

Foto di Zulfugar Karimov su Unsplash

GuidePrincipiante35 min Flutter 3.x

Menu a tendina in Flutter: DropdownButton, DropdownButtonFormField e DropdownMenu

Quasi ogni app ha bisogno di far scegliere all'utente una voce da un elenco: la lingua, la categoria di un prodotto, la città di residenza.

In Flutter esistono tre strumenti principali per farlo:

  • DropdownButton: il menu a tendina classico, semplice e immediato;
  • DropdownButtonFormField: la versione integrata nei Form, con validazione e decorazione;
  • DropdownMenu: il widget introdotto con Material 3, con campo di testo, filtro e ricerca.

In questo tutorial li vedremo tutti e tre partendo da zero, con esempi completi e funzionanti. Alla fine costruiremo anche due dropdown collegati (regione → provincia), un pattern molto comune nei form reali.

Prerequisiti: un progetto Flutter già creato e le basi di StatefulWidget e setState.

  1. 1

    Preparare il progetto e i dati da mostrare

    Creiamo un nuovo progetto (o apriamone uno esistente) e prepariamo una schermata vuota con un Scaffold.

    Un menu a tendina mostra una lista di opzioni: per tenere il codice ordinato conviene definirla come costante, separata dalla UI. In questo primo esempio usiamo una semplice lista di stringhe.

    Assicurati di avere useMaterial3: true nel tema: è il default nei progetti Flutter 3.x recenti e serve per l'esempio con DropdownMenu che vedremo più avanti.

    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: 'Dropdown Demo',
          theme: ThemeData(
            colorSchemeSeed: Colors.indigo,
            useMaterial3: true,
          ),
          home: const HomePage(),
        );
      }
    }
    
    /// Le opzioni che mostreremo nel menu a tendina.
    const List<String> kLinguaggi = <String>[
      'Dart',
      'Kotlin',
      'Swift',
      'JavaScript',
      'Python',
    ];
    
    class HomePage extends StatelessWidget {
      const HomePage({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('Menu a tendina')),
          body: const Padding(
            padding: EdgeInsets.all(16),
            child: Center(child: Text('Qui metteremo i dropdown')),
          ),
        );
      }
    }

    Risultato atteso

    L'app si avvia e mostra una schermata vuota con la AppBar "Menu a tendina".

  2. 2

    Il primo DropdownButton con stato

    DropdownButton è un widget controllato: non memorizza da solo la scelta dell'utente. Tu gli passi il valore corrente tramite value e lui ti notifica i cambiamenti con onChanged. Per questo deve vivere dentro uno StatefulWidget.

    Tre proprietà fondamentali:

    • items: la lista di DropdownMenuItem, ognuno con un value e un child (il widget mostrato);
    • value: il valore selezionato; può essere null (allora viene mostrato l'hint);
    • onChanged: callback invocata alla selezione. Se la imposti a null, il dropdown appare disabilitato.

    ⚠️ Regola d'oro: il value deve essere null oppure corrispondere esattamente a uno dei value presenti in items, altrimenti Flutter lancia un'asserzione in fase di build.

    class LinguaggioPicker extends StatefulWidget {
      const LinguaggioPicker({super.key});
    
      @override
      State<LinguaggioPicker> createState() => _LinguaggioPickerState();
    }
    
    class _LinguaggioPickerState extends State<LinguaggioPicker> {
      String? _selezionato; // all'inizio nessuna scelta
    
      @override
      Widget build(BuildContext context) {
        return Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            DropdownButton<String>(
              value: _selezionato,
              hint: const Text('Scegli un linguaggio'),
              items: kLinguaggi.map((String linguaggio) {
                return DropdownMenuItem<String>(
                  value: linguaggio,
                  child: Text(linguaggio),
                );
              }).toList(),
              onChanged: (String? nuovo) {
                setState(() => _selezionato = nuovo);
              },
            ),
            const SizedBox(height: 12),
            Text(
              _selezionato == null
                  ? 'Nessuna selezione'
                  : 'Hai scelto: $_selezionato',
            ),
          ],
        );
      }
    }

    Risultato atteso

    Toccando il dropdown si apre l'elenco dei cinque linguaggi; dopo la selezione il testo sotto mostra la voce scelta.

  3. 3

    Personalizzare l'aspetto del dropdown

    Il DropdownButton di default è piuttosto spartano. Ecco le proprietà che userai più spesso:

    • isExpanded: true: fa occupare al dropdown tutta la larghezza disponibile (evita anche gli overflow con testi lunghi);
    • icon, iconSize, iconEnabledColor: personalizzano la freccia;
    • underline: sostituisce (o rimuove, con un SizedBox.shrink()) la linea in basso;
    • borderRadius e dropdownColor: aspetto del menu che si apre;
    • menuMaxHeight: limita l'altezza quando le voci sono tante.

    Se vuoi un bordo completo, il trucco più semplice è avvolgere il dropdown in un InputDecorator oppure usare direttamente DropdownButtonFormField, che vedremo nel passo successivo.

    Nota anche DropdownButtonHideUnderline: utile quando inserisci il dropdown dentro un contenitore già decorato.

    Container(
      padding: const EdgeInsets.symmetric(horizontal: 12),
      decoration: BoxDecoration(
        border: Border.all(color: Colors.grey.shade400),
        borderRadius: BorderRadius.circular(12),
      ),
      child: DropdownButtonHideUnderline(
        child: DropdownButton<String>(
          value: _selezionato,
          isExpanded: true,
          hint: const Text('Scegli un linguaggio'),
          icon: const Icon(Icons.expand_more),
          borderRadius: BorderRadius.circular(12),
          menuMaxHeight: 300,
          items: kLinguaggi.map((String linguaggio) {
            return DropdownMenuItem<String>(
              value: linguaggio,
              child: Row(
                children: [
                  const Icon(Icons.code, size: 18),
                  const SizedBox(width: 8),
                  Text(linguaggio),
                ],
              ),
            );
          }).toList(),
          onChanged: (String? nuovo) => setState(() => _selezionato = nuovo),
        ),
      ),
    )

    Risultato atteso

    Il dropdown occupa tutta la larghezza, ha un bordo arrotondato, nessuna underline e ogni voce mostra un'icona accanto al testo.

  4. 4

    Dropdown dentro un Form con validazione

    Quando il menu a tendina fa parte di un modulo, usa DropdownButtonFormField: eredita tutte le opzioni di DropdownButton e in più supporta decoration (come un TextFormField), validator e il salvataggio con FormState.

    Punti chiave:

    • il valore iniziale si passa con initialValue (nelle versioni meno recenti di Flutter la proprietà si chiama value: se l'IDE segnala un errore, usa quella);
    • il validator restituisce null se il valore è valido, altrimenti il messaggio di errore;
    • _formKey.currentState!.validate() esegue la validazione di tutti i campi del form.

    In questo modo l'errore viene mostrato sotto il campo, esattamente come per un TextFormField.

    class IscrizioneForm extends StatefulWidget {
      const IscrizioneForm({super.key});
    
      @override
      State<IscrizioneForm> createState() => _IscrizioneFormState();
    }
    
    class _IscrizioneFormState extends State<IscrizioneForm> {
      final _formKey = GlobalKey<FormState>();
      String? _linguaggio;
    
      void _invia() {
        if (_formKey.currentState!.validate()) {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text('Iscrizione inviata: $_linguaggio')),
          );
        }
      }
    
      @override
      Widget build(BuildContext context) {
        return Form(
          key: _formKey,
          child: Column(
            children: [
              DropdownButtonFormField<String>(
                initialValue: _linguaggio,
                isExpanded: true,
                decoration: const InputDecoration(
                  labelText: 'Linguaggio preferito',
                  prefixIcon: Icon(Icons.terminal),
                  border: OutlineInputBorder(),
                ),
                items: kLinguaggi
                    .map((l) => DropdownMenuItem(value: l, child: Text(l)))
                    .toList(),
                validator: (value) =>
                    value == null ? 'Seleziona un linguaggio' : null,
                onChanged: (value) => setState(() => _linguaggio = value),
              ),
              const SizedBox(height: 16),
              ElevatedButton(
                onPressed: _invia,
                child: const Text('Invia'),
              ),
            ],
          ),
        );
      }
    }

    Risultato atteso

    Premendo "Invia" senza aver scelto nulla compare in rosso "Seleziona un linguaggio"; dopo la selezione la SnackBar mostra il valore scelto.

  5. 5

    Il DropdownMenu di Material 3 con filtro e ricerca

    DropdownMenu è il widget introdotto con Material 3: unisce un campo di testo a un menu a tendina, quindi l'utente può digitare per filtrare le voci.

    Differenze principali rispetto a DropdownButton:

    • le voci si passano in dropdownMenuEntries come DropdownMenuEntry (con value, label e, opzionalmente, leadingIcon);
    • la selezione si legge da onSelected;
    • enableFilter: true filtra le voci mentre si scrive, enableSearch (attivo di default) evidenzia la prima corrispondenza;
    • requestFocusOnTap: false lo rende di sola selezione, senza tastiera (utile su mobile).

    È ottimo per elenchi lunghi come paesi, valute o categorie.

    class PaesePicker extends StatefulWidget {
      const PaesePicker({super.key});
    
      @override
      State<PaesePicker> createState() => _PaesePickerState();
    }
    
    class _PaesePickerState extends State<PaesePicker> {
      static const paesi = <String>[
        'Italia', 'Francia', 'Germania', 'Spagna', 'Portogallo', 'Grecia',
      ];
    
      String? _paese;
    
      @override
      Widget build(BuildContext context) {
        return Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            DropdownMenu<String>(
              width: double.infinity,
              label: const Text('Paese'),
              leadingIcon: const Icon(Icons.public),
              enableFilter: true,
              menuHeight: 280,
              initialSelection: _paese,
              onSelected: (String? valore) => setState(() => _paese = valore),
              dropdownMenuEntries: paesi
                  .map((p) => DropdownMenuEntry<String>(value: p, label: p))
                  .toList(),
            ),
            const SizedBox(height: 8),
            Text('Paese selezionato: ${_paese ?? "-"}'),
          ],
        );
      }
    }

    Risultato atteso

    Digitando "ita" nel campo l'elenco si riduce a "Italia"; selezionandola il testo sotto si aggiorna.

  6. 6

    Due dropdown collegati: regione e provincia

    Un classico dei form reali: il secondo menu dipende dalla scelta fatta nel primo.

    Il pattern è semplice ma ha una trappola molto comune: quando cambi la regione devi azzerare la provincia, altrimenti il value del secondo dropdown non esisterà più nella nuova lista di items e l'app andrà in errore con un'asserzione.

    Nota anche che il secondo dropdown passa onChanged: null finché non è stata scelta una regione: è il modo idiomatico per disabilitarlo.

    class RegioneProvincia extends StatefulWidget {
      const RegioneProvincia({super.key});
    
      @override
      State<RegioneProvincia> createState() => _RegioneProvinciaState();
    }
    
    class _RegioneProvinciaState extends State<RegioneProvincia> {
      static const Map<String, List<String>> dati = {
        'Lombardia': ['Milano', 'Bergamo', 'Brescia'],
        'Lazio': ['Roma', 'Latina', 'Viterbo'],
        'Sicilia': ['Palermo', 'Catania', 'Messina'],
      };
    
      String? _regione;
      String? _provincia;
    
      @override
      Widget build(BuildContext context) {
        final province = _regione == null ? <String>[] : dati[_regione]!;
    
        return Column(
          children: [
            DropdownButtonFormField<String>(
              initialValue: _regione,
              isExpanded: true,
              decoration: const InputDecoration(
                labelText: 'Regione',
                border: OutlineInputBorder(),
              ),
              items: dati.keys
                  .map((r) => DropdownMenuItem(value: r, child: Text(r)))
                  .toList(),
              onChanged: (valore) {
                setState(() {
                  _regione = valore;
                  _provincia = null; // fondamentale: azzera la scelta precedente
                });
              },
            ),
            const SizedBox(height: 16),
            DropdownButtonFormField<String>(
              initialValue: _provincia,
              isExpanded: true,
              decoration: const InputDecoration(
                labelText: 'Provincia',
                border: OutlineInputBorder(),
              ),
              items: province
                  .map((p) => DropdownMenuItem(value: p, child: Text(p)))
                  .toList(),
              onChanged: _regione == null
                  ? null // disabilitato finché non scegli la regione
                  : (valore) => setState(() => _provincia = valore),
            ),
          ],
        );
      }
    }

    Risultato atteso

    Il dropdown delle province è grigio e non cliccabile finché non si sceglie una regione; cambiando regione la provincia si svuota e mostra le nuove voci.

  7. 7

    Errori frequenti e buone pratiche

    Prima di chiudere, i problemi che incontrerai più spesso e come risolverli:

    1. "There should be exactly one item with DropdownButton's value" — il value non è presente negli items, oppure ci sono voci duplicate. Soluzione: azzera il valore quando cambi la lista (come nel passo precedente) e assicurati che i value siano univoci.
    2. Il menu non si aggiorna dopo la selezione — hai dimenticato setState dentro onChanged, oppure stai usando uno StatelessWidget.
    3. Overflow del testo — imposta isExpanded: true e, sulle voci lunghe, Text(..., overflow: TextOverflow.ellipsis).
    4. Oggetti invece di stringhe — puoi usare qualsiasi tipo come value (anche un enum o un modello). Se usi classi personalizzate, implementa == e hashCode (o usa un enum), perché Flutter confronta i valori per uguaglianza.
    5. Liste molto lunghe — preferisci DropdownMenu con enableFilter: true, molto più comodo per l'utente.

    Qui sotto un esempio con un enum, l'approccio più sicuro e leggibile quando le opzioni sono fisse.

    enum Spedizione {
      standard('Standard - 5 giorni'),
      express('Express - 24 ore'),
      ritiro('Ritiro in negozio');
    
      const Spedizione(this.etichetta);
      final String etichetta;
    }
    
    // Dentro uno StatefulWidget:
    // Spedizione _spedizione = Spedizione.standard;
    
    DropdownButton<Spedizione>(
      value: _spedizione,
      isExpanded: true,
      items: Spedizione.values
          .map((s) => DropdownMenuItem(
                value: s,
                child: Text(s.etichetta, overflow: TextOverflow.ellipsis),
              ))
          .toList(),
      onChanged: (valore) {
        if (valore != null) setState(() => _spedizione = valore);
      },
    )

    Risultato atteso

    Il dropdown mostra le tre modalità di spedizione e il valore selezionato è tipizzato (`Spedizione`), senza stringhe magiche nel codice.

CondividiXLinkedInFacebookWhatsApp

Commenti (0)

Ancora nessun commento. Inizia tu!