Input di base in Flutter: TextField, bottoni e gesti onTap

Foto di Chase Chappell su Unsplash

GuidePrincipiante35 min Flutter 3.x

Input di base in Flutter: TextField, bottoni e gesti onTap

Ogni app ha bisogno di ricevere input dall'utente: un nome da digitare, un bottone da premere, una card da toccare. In Flutter questi tre elementi si traducono in TextField, nei widget della famiglia Button e nei rilevatori di gesti come InkWell e GestureDetector.

In questo tutorial per principianti costruiremo una piccola schermata "Saluta l'utente": l'utente scrive il proprio nome, preme un bottone e vede comparire un messaggio personalizzato. Lungo la strada vedremo come:

  • leggere il testo digitato con un TextEditingController;
  • personalizzare l'aspetto di un campo con InputDecoration;
  • usare ElevatedButton, OutlinedButton, TextButton e IconButton;
  • rendere "toccabile" qualsiasi widget con InkWell e GestureDetector;
  • abilitare o disabilitare un bottone in base a ciò che l'utente scrive;
  • liberare le risorse con dispose() e chiudere la tastiera.

Serve solo un progetto Flutter appena creato (flutter create input_demo) e la voglia di sporcarsi le mani con setState.

  1. 1

    Preparare la schermata di partenza

    Creiamo un nuovo progetto e sostituiamo il contenuto di lib/main.dart con una schermata minimale. Usiamo uno StatefulWidget perché più avanti dovremo aggiornare l'interfaccia in risposta all'input dell'utente.

    Alcune note sul codice:

    • MaterialApp fornisce tema, routing e stile Material;
    • Scaffold dà la struttura base (AppBar + body);
    • SingleChildScrollView evita l'errore di overflow quando la tastiera compare e riduce lo spazio disponibile: è uno degli inconvenienti più comuni per chi inizia.
    import 'package:flutter/material.dart';
    
    void main() => runApp(const InputDemoApp());
    
    class InputDemoApp extends StatelessWidget {
      const InputDemoApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Input di base',
          theme: ThemeData(
            colorSchemeSeed: Colors.indigo,
            useMaterial3: true,
          ),
          home: const HomePage(),
        );
      }
    }
    
    class HomePage extends StatefulWidget {
      const HomePage({super.key});
    
      @override
      State<HomePage> createState() => _HomePageState();
    }
    
    class _HomePageState extends State<HomePage> {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('Input di base')),
          body: const SingleChildScrollView(
            padding: EdgeInsets.all(24),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                Text('Come ti chiami?', style: TextStyle(fontSize: 20)),
              ],
            ),
          ),
        );
      }
    }

    Risultato atteso

    Avviando l'app con `flutter run` vedi una AppBar con il titolo "Input di base" e il testo "Come ti chiami?" nel corpo della pagina.

  2. 2

    Aggiungere un TextField e leggerne il valore

    Il widget TextField mostra un campo di testo. Per leggere ciò che l'utente digita il modo più comune è collegargli un TextEditingController.

    Regole d'oro:

    1. il controller si crea in initState() (o come campo final);
    2. va sempre distrutto in dispose(), altrimenti si crea un memory leak;
    3. il valore corrente si legge con controller.text.

    Con InputDecoration personalizziamo etichetta, bordo e icona. Le proprietà textInputAction e onSubmitted gestiscono il tasto "Invio" della tastiera software.

    class _HomePageState extends State<HomePage> {
      final TextEditingController _nomeController = TextEditingController();
      String _saluto = '';
    
      @override
      void dispose() {
        _nomeController.dispose(); // libera le risorse
        super.dispose();
      }
    
      void _saluta() {
        final nome = _nomeController.text.trim();
        setState(() {
          _saluto = nome.isEmpty ? 'Ciao sconosciuto!' : 'Ciao $nome!';
        });
        FocusScope.of(context).unfocus(); // chiude la tastiera
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('Input di base')),
          body: SingleChildScrollView(
            padding: const EdgeInsets.all(24),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                const Text('Come ti chiami?', style: TextStyle(fontSize: 20)),
                const SizedBox(height: 16),
                TextField(
                  controller: _nomeController,
                  textInputAction: TextInputAction.done,
                  textCapitalization: TextCapitalization.words,
                  onSubmitted: (_) => _saluta(),
                  decoration: const InputDecoration(
                    labelText: 'Nome',
                    hintText: 'Es. Giulia',
                    prefixIcon: Icon(Icons.person_outline),
                    border: OutlineInputBorder(),
                  ),
                ),
                const SizedBox(height: 24),
                Text(_saluto, style: const TextStyle(fontSize: 22)),
              ],
            ),
          ),
        );
      }
    }

    Risultato atteso

    Digitando un nome e premendo "Invio" sulla tastiera compare il messaggio "Ciao <nome>!" e la tastiera si chiude.

  3. 3

    Aggiungere i bottoni: ElevatedButton, OutlinedButton e TextButton

    In Material 3 i bottoni principali sono:

    Widget Quando usarlo
    ElevatedButton azione primaria, ben visibile
    FilledButton alternativa piena all'elevated
    OutlinedButton azione secondaria
    TextButton azione terziaria / link
    IconButton sola icona, es. in AppBar

    Tutti accettano il parametro onPressed: se gli passi null il bottone appare disabilitato. È un dettaglio importante e spesso ignorato dai principianti.

    Aggiungiamo una riga con il bottone "Saluta" e uno per pulire il campo. Nota _nomeController.clear(), che svuota il testo.

    // dentro la Column, dopo il TextField
    const SizedBox(height: 16),
    Row(
      children: [
        Expanded(
          child: ElevatedButton.icon(
            onPressed: _saluta,
            icon: const Icon(Icons.waving_hand_outlined),
            label: const Text('Saluta'),
          ),
        ),
        const SizedBox(width: 12),
        OutlinedButton(
          onPressed: () {
            _nomeController.clear();
            setState(() => _saluto = '');
          },
          child: const Text('Pulisci'),
        ),
      ],
    ),
    TextButton(
      onPressed: () {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('Scrivi il tuo nome e premi Saluta')),
        );
      },
      child: const Text('Come funziona?'),
    ),

    Risultato atteso

    Sotto al campo compaiono tre bottoni: "Saluta" mostra il messaggio, "Pulisci" svuota tutto, "Come funziona?" apre una SnackBar in basso.

  4. 4

    Abilitare il bottone solo quando c'è del testo

    Per reagire mentre l'utente scrive abbiamo due strade:

    • la callback onChanged del TextField;
    • un listener sul TextEditingController (utile se il testo cambia anche da codice).

    Usiamo la seconda: aggiungiamo un listener in initState() che chiama setState, così a ogni carattere digitato la UI si ricostruisce. Il bottone riceve onPressed: null quando il campo è vuoto e Flutter lo mostra automaticamente in grigio.

    ⚠️ Ricordati di rimuovere il listener (o semplicemente di fare dispose() del controller, che lo elimina insieme a sé) per evitare chiamate a setState su un widget non più montato.

    @override
    void initState() {
      super.initState();
      _nomeController.addListener(_onTestoCambiato);
    }
    
    void _onTestoCambiato() {
      setState(() {}); // ricostruisce la UI a ogni carattere
    }
    
    @override
    void dispose() {
      _nomeController.removeListener(_onTestoCambiato);
      _nomeController.dispose();
      super.dispose();
    }
    
    bool get _puoSalutare => _nomeController.text.trim().isNotEmpty;
    
    // nel build, sostituisci l'ElevatedButton con:
    ElevatedButton.icon(
      onPressed: _puoSalutare ? _saluta : null, // null = disabilitato
      icon: const Icon(Icons.waving_hand_outlined),
      label: Text(_puoSalutare ? 'Saluta' : 'Scrivi un nome'),
    )

    Risultato atteso

    Il bottone "Saluta" resta grigio e non cliccabile finché il campo è vuoto; appena digiti un carattere si attiva e cambia etichetta.

  5. 5

    Rendere toccabile qualsiasi widget con InkWell e GestureDetector

    Non solo i bottoni possono essere premuti: qualsiasi widget può diventare interattivo.

    • InkWell: aggiunge il tocco e l'effetto ripple di Material. Va inserito dentro un Material (la Card lo è già) e conviene dargli lo stesso borderRadius del contenitore.
    • GestureDetector: più generico, riconosce tap, doppio tap, long press, drag… ma non disegna nessun feedback visivo.

    Aggiungiamo una card con dei saluti rapidi: toccandola il nome viene inserito nel campo. E una piccola area con GestureDetector che conta i doppi tap.

    // helper dentro lo State
    void _usaNome(String nome) {
      _nomeController.text = nome;
      _nomeController.selection =
          TextSelection.collapsed(offset: nome.length); // cursore a fine testo
      _saluta();
    }
    
    // nel build, dopo i bottoni
    const SizedBox(height: 24),
    const Text('Nomi rapidi', style: TextStyle(fontWeight: FontWeight.bold)),
    const SizedBox(height: 8),
    Wrap(
      spacing: 8,
      children: ['Giulia', 'Marco', 'Sara'].map((nome) {
        return Card(
          clipBehavior: Clip.antiAlias,
          child: InkWell(
            onTap: () => _usaNome(nome),
            onLongPress: () => ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(content: Text('Tocco prolungato su $nome')),
            ),
            child: Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
              child: Text(nome),
            ),
          ),
        );
      }).toList(),
    ),
    const SizedBox(height: 24),
    GestureDetector(
      onDoubleTap: () => setState(() => _saluto = 'Doppio tap ricevuto!'),
      behavior: HitTestBehavior.opaque, // rileva il tocco anche sulle aree vuote
      child: Container(
        height: 80,
        alignment: Alignment.center,
        decoration: BoxDecoration(
          color: Colors.indigo.withValues(alpha: 0.08),
          borderRadius: BorderRadius.circular(12),
        ),
        child: const Text('Fai doppio tap qui'),
      ),
    ),

    Risultato atteso

    Toccando una card il nome finisce nel campo e appare subito il saluto; con il doppio tap sull'area colorata il messaggio cambia in "Doppio tap ricevuto!".

  6. 6

    Rifinire: campo password, IconButton e tastiera dedicata

    Per completare il quadro degli input di base vediamo tre rifiniture molto usate:

    1. obscureText per le password, con un IconButton come suffixIcon per mostrare/nascondere il testo;
    2. keyboardType per scegliere la tastiera giusta (TextInputType.emailAddress, .number, .phone…);
    3. maxLength per limitare i caratteri e mostrare il contatore.

    Questi tre parametri fanno una differenza enorme nella qualità percepita dell'app e costano una riga di codice ciascuno.

    // nuovo campo dello State
    bool _nascondiPassword = true;
    final TextEditingController _passwordController = TextEditingController();
    
    // ricordati di fare _passwordController.dispose() in dispose()
    
    // nel build, dentro la Column
    const SizedBox(height: 24),
    TextField(
      controller: _passwordController,
      obscureText: _nascondiPassword,
      keyboardType: TextInputType.visiblePassword,
      maxLength: 20,
      decoration: InputDecoration(
        labelText: 'Password',
        border: const OutlineInputBorder(),
        prefixIcon: const Icon(Icons.lock_outline),
        suffixIcon: IconButton(
          tooltip: _nascondiPassword ? 'Mostra' : 'Nascondi',
          icon: Icon(
            _nascondiPassword ? Icons.visibility_off : Icons.visibility,
          ),
          onPressed: () =>
              setState(() => _nascondiPassword = !_nascondiPassword),
        ),
      ),
    ),

    Risultato atteso

    Il campo password mostra i pallini, il contatore "0/20" e l'icona a forma di occhio che alterna testo visibile e nascosto.

  7. 7

    Errori tipici e prossimi passi

    Ricapitoliamo gli inciampi più comuni con gli input di base:

    • Dimenticare dispose() del controller → memory leak e warning in console.
    • Usare setState in un StatelessWidget → non esiste: serve uno StatefulWidget.
    • Overflow quando appare la tastiera → avvolgi il contenuto in SingleChildScrollView o usa resizeToAvoidBottomInset.
    • Impostare sia controller sia initialValue → Flutter lancia un'asserzione: scegline uno solo (initialValue esiste su TextFormField, non su TextField).
    • GestureDetector che non rileva i tocchi sulle zone trasparenti → imposta behavior: HitTestBehavior.opaque.
    • Leggere controller.text prima che l'utente scriva → ottieni una stringa vuota, non null: usa trim().isEmpty per i controlli.

    Da qui puoi proseguire con la validazione strutturata (Form + TextFormField), la gestione del focus tra più campi con FocusNode, oppure spostare lo stato fuori dal widget con Provider o Riverpod.

    // Bonus: chiudere la tastiera toccando ovunque sullo sfondo
    @override
    Widget build(BuildContext context) {
      return GestureDetector(
        onTap: () => FocusScope.of(context).unfocus(),
        child: Scaffold(
          appBar: AppBar(title: const Text('Input di base')),
          body: /* ... il tuo contenuto ... */ const SizedBox.shrink(),
        ),
      );
    }

    Risultato atteso

    L'app funziona senza warning: toccando lo sfondo la tastiera si chiude e tutti i controller vengono correttamente rilasciati alla chiusura della schermata.

CondividiXLinkedInFacebookWhatsApp

Commenti (0)

Ancora nessun commento. Inizia tu!