I pulsanti in Flutter: ElevatedButton, TextButton, OutlinedButton e IconButton

Foto di Brett Jordan su Unsplash

GuidePrincipiante30 min Flutter 3.x

I pulsanti in Flutter: ElevatedButton, TextButton, OutlinedButton e IconButton

I pulsanti sono il primo punto di contatto tra l'utente e la logica della tua app: senza un tocco, non succede nulla.

Flutter offre una famiglia completa di pulsanti Material 3 — ElevatedButton, FilledButton, TextButton, OutlinedButton, IconButton — che condividono la stessa API e si differenziano solo per l'enfasi visiva.

In questo tutorial per principianti vedremo:

  • quale pulsante usare in base all'importanza dell'azione;
  • come gestire il callback onPressed e lo stato disabilitato;
  • come aggiungere un'icona con .icon();
  • come personalizzare colori, forma e padding con ButtonStyle e styleFrom;
  • come definire uno stile globale nel ThemeData per non ripetere il codice.

Al termine avrai una schermata dimostrativa con tutti i tipi di pulsante e un pulsante "Salva" che mostra un indicatore di caricamento mentre lavora.

  1. 1

    Preparare il progetto e la schermata di prova

    Crea un nuovo progetto (o apri il tuo) e sostituisci il contenuto di lib/main.dart con una struttura minima: MaterialApp con Material 3 attivo e uno Scaffold che ospiterà i nostri pulsanti.

    Usiamo ColorScheme.fromSeed per generare una palette coerente: i pulsanti Material 3 prendono automaticamente i colori dal tema, quindi cambiando il seedColor cambierà l'aspetto di tutti i bottoni.

    Il body è un ListView con padding: così, anche aggiungendo molti pulsanti, la pagina resta scorrevole e non genera errori di overflow.

    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: 'Pulsanti Flutter',
          debugShowCheckedModeBanner: false,
          theme: ThemeData(
            useMaterial3: true,
            colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
          ),
          home: const ButtonsPage(),
        );
      }
    }
    
    class ButtonsPage extends StatelessWidget {
      const ButtonsPage({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('Pulsanti in Flutter')),
          body: ListView(
            padding: const EdgeInsets.all(16),
            children: const [
              Text('Qui aggiungeremo i pulsanti'),
            ],
          ),
        );
      }
    }

    Risultato atteso

    L'app si avvia mostrando una AppBar con il titolo "Pulsanti in Flutter" e il testo segnaposto nel corpo.

  2. 2

    I quattro pulsanti base e la loro gerarchia visiva

    Material 3 propone una scala di enfasi. Scegli il pulsante in base a quanto è importante l'azione:

    Widget Enfasi Quando usarlo
    FilledButton Massima Azione principale della schermata (Salva, Acquista)
    ElevatedButton Alta Azione importante su sfondi "pieni" o con contenuti sotto
    OutlinedButton Media Azione secondaria (Annulla, Modifica)
    TextButton Bassa Azioni terziarie, link, dialog

    Tutti accettano gli stessi parametri fondamentali:

    • onPressed: la funzione chiamata al tocco;
    • child: il contenuto, di solito un Text.

    Regola d'oro: se passi null a onPressed, il pulsante diventa automaticamente disabilitato e grigio. Non serve nessuna proprietà enabled.

    Sostituisci i children del ListView con l'elenco qui sotto.

    children: [
      FilledButton(
        onPressed: () => debugPrint('FilledButton premuto'),
        child: const Text('Filled (azione principale)'),
      ),
      const SizedBox(height: 12),
      ElevatedButton(
        onPressed: () => debugPrint('ElevatedButton premuto'),
        child: const Text('Elevated'),
      ),
      const SizedBox(height: 12),
      OutlinedButton(
        onPressed: () => debugPrint('OutlinedButton premuto'),
        child: const Text('Outlined (secondaria)'),
      ),
      const SizedBox(height: 12),
      TextButton(
        onPressed: () => debugPrint('TextButton premuto'),
        child: const Text('Text (terziaria)'),
      ),
      const SizedBox(height: 24),
      const ElevatedButton(
        onPressed: null, // disabilitato
        child: Text('Disabilitato'),
      ),
    ],

    Risultato atteso

    Vedi quattro pulsanti con enfasi crescente/decrescente e uno grigio non cliccabile. Toccandoli, i messaggi compaiono nella console di debug.

  3. 3

    Pulsanti con icona e IconButton

    Ogni pulsante ha un costruttore .icon() che affianca un'icona all'etichetta: ElevatedButton.icon, FilledButton.icon, OutlinedButton.icon, TextButton.icon. I parametri sono icon e label.

    Quando invece vuoi solo l'icona (tipico nelle AppBar o nelle liste) usi IconButton, che richiede icon e opzionalmente tooltip per l'accessibilità.

    Per allineare più pulsanti sulla stessa riga usa un Row con MainAxisAlignment, oppure Wrap se vuoi che vadano a capo su schermi stretti: è la scelta più sicura per evitare overflow.

    Wrap(
      spacing: 12,
      runSpacing: 12,
      children: [
        FilledButton.icon(
          onPressed: () {},
          icon: const Icon(Icons.download),
          label: const Text('Scarica'),
        ),
        OutlinedButton.icon(
          onPressed: () {},
          icon: const Icon(Icons.share),
          label: const Text('Condividi'),
        ),
        IconButton(
          onPressed: () {},
          tooltip: 'Preferiti',
          icon: const Icon(Icons.favorite_border),
        ),
        IconButton.filledTonal(
          onPressed: () {},
          tooltip: 'Elimina',
          icon: const Icon(Icons.delete_outline),
        ),
      ],
    ),

    Risultato atteso

    Una riga di pulsanti con icona che va a capo automaticamente sugli schermi stretti; tenendo premuto su un IconButton compare il tooltip.

  4. 4

    Personalizzare l'aspetto con styleFrom

    Per cambiare colori, forma, dimensioni e padding di un singolo pulsante usa il metodo statico styleFrom della classe corrispondente (ElevatedButton.styleFrom, FilledButton.styleFrom, ecc.). Restituisce un ButtonStyle da passare al parametro style.

    Le proprietà più usate:

    • backgroundColor / foregroundColor: sfondo e colore di testo e icona;
    • padding: spazio interno;
    • shape: forma del bordo, tipicamente RoundedRectangleBorder;
    • minimumSize: dimensione minima, utile per un pulsante a tutta larghezza con Size.fromHeight(52);
    • textStyle: stile del testo.

    💡 Suggerimento: per un pulsante largo quanto lo schermo, minimumSize: const Size.fromHeight(52) è più pulito che avvolgerlo in un SizedBox(width: double.infinity).

    FilledButton(
      style: FilledButton.styleFrom(
        backgroundColor: Colors.deepOrange,
        foregroundColor: Colors.white,
        minimumSize: const Size.fromHeight(52),
        padding: const EdgeInsets.symmetric(horizontal: 24),
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(12),
        ),
        textStyle: const TextStyle(
          fontSize: 16,
          fontWeight: FontWeight.bold,
        ),
      ),
      onPressed: () {},
      child: const Text('Acquista ora'),
    ),
    const SizedBox(height: 12),
    OutlinedButton(
      style: OutlinedButton.styleFrom(
        foregroundColor: Colors.deepOrange,
        side: const BorderSide(color: Colors.deepOrange, width: 1.5),
        minimumSize: const Size.fromHeight(52),
        shape: const StadiumBorder(),
      ),
      onPressed: () {},
      child: const Text('Aggiungi al carrello'),
    ),

    Risultato atteso

    Un pulsante arancione a tutta larghezza con angoli arrotondati e, sotto, un pulsante bordato a forma di pillola.

  5. 5

    Un pulsante con stato di caricamento

    Un errore classico è permettere all'utente di premere più volte lo stesso pulsante mentre un'operazione è in corso (ad esempio un salvataggio in rete).

    La soluzione è semplice: in uno StatefulWidget teniamo una variabile booleana _loading. Quando è true passiamo null a onPressed (pulsante disabilitato) e mostriamo un CircularProgressIndicator al posto del testo.

    Nota due dettagli importanti:

    • l'indicatore va incapsulato in un SizedBox di dimensioni piccole (es. 20×20), altrimenti "gonfia" il pulsante;
    • dopo un await verifica sempre if (!mounted) return; prima di chiamare setState, perché l'utente potrebbe aver lasciato la schermata.
    class SaveButton extends StatefulWidget {
      const SaveButton({super.key});
    
      @override
      State<SaveButton> createState() => _SaveButtonState();
    }
    
    class _SaveButtonState extends State<SaveButton> {
      bool _loading = false;
    
      Future<void> _save() async {
        setState(() => _loading = true);
        await Future.delayed(const Duration(seconds: 2)); // simula la rete
        if (!mounted) return;
        setState(() => _loading = false);
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('Salvataggio completato')),
        );
      }
    
      @override
      Widget build(BuildContext context) {
        return FilledButton(
          onPressed: _loading ? null : _save,
          style: FilledButton.styleFrom(
            minimumSize: const Size.fromHeight(52),
          ),
          child: _loading
              ? const SizedBox(
                  width: 20,
                  height: 20,
                  child: CircularProgressIndicator(
                    strokeWidth: 2,
                    color: Colors.white,
                  ),
                )
              : const Text('Salva'),
        );
      }
    }

    Risultato atteso

    Premendo "Salva" il pulsante si disabilita e mostra uno spinner per 2 secondi, poi appare una SnackBar di conferma.

  6. 6

    Definire lo stile globale nel tema

    Se tutti i pulsanti dell'app devono avere lo stesso raggio degli angoli e la stessa altezza, ripetere styleFrom ovunque è una cattiva pratica. Meglio configurarlo una sola volta nel ThemeData con i temi dedicati:

    • filledButtonThemeFilledButtonThemeData
    • elevatedButtonThemeElevatedButtonThemeData
    • outlinedButtonThemeOutlinedButtonThemeData
    • textButtonThemeTextButtonThemeData
    • iconButtonThemeIconButtonThemeData

    Da quel momento ogni pulsante eredita lo stile; se in un caso specifico serve un'eccezione, basta passare comunque un style locale, che ha la precedenza.

    Ricompila e verifica: tutti i pulsanti senza style esplicito adottano il nuovo aspetto.

    theme: ThemeData(
      useMaterial3: true,
      colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
      filledButtonTheme: FilledButtonThemeData(
        style: FilledButton.styleFrom(
          minimumSize: const Size.fromHeight(52),
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(12),
          ),
          textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
        ),
      ),
      outlinedButtonTheme: OutlinedButtonThemeData(
        style: OutlinedButton.styleFrom(
          minimumSize: const Size.fromHeight(52),
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(12),
          ),
        ),
      ),
      textButtonTheme: TextButtonThemeData(
        style: TextButton.styleFrom(
          textStyle: const TextStyle(fontWeight: FontWeight.w600),
        ),
      ),
    ),

    Risultato atteso

    Tutti i pulsanti dell'app hanno altezza 52 e angoli arrotondati a 12 px senza ripetere codice, tranne quelli con uno stile locale personalizzato.

CondividiXLinkedInFacebookWhatsApp

Commenti (0)

Ancora nessun commento. Inizia tu!