Introduzione

La raccolta di dati dall'utente è uno dei compiti più comuni nello sviluppo di applicazioni mobili: schermate di login, registrazione, checkout, profili. Flutter offre un sistema potente e flessibile per gestire i form attraverso il widget Form e i campi TextFormField. In questo articolo vedremo come costruire form solidi, validare gli input e organizzare il codice in modo pulito e riutilizzabile.

I componenti fondamentali

Il sistema dei form in Flutter si basa su tre elementi principali:

  • Form: un widget contenitore che raggruppa più campi e ne coordina validazione, salvataggio e reset.
  • GlobalKey<FormState>: una chiave che permette di accedere allo stato del form e invocare metodi come validate() o save().
  • TextFormField: una versione di TextField integrata con il sistema di validazione.

Un primo form di login

Vediamo un esempio base con email e password:

class LoginForm extends StatefulWidget {
  const LoginForm({super.key});

  @override
  State<LoginForm> createState() => _LoginFormState();
}

class _LoginFormState extends State<LoginForm> {
  final _formKey = GlobalKey<FormState>();
  String _email = '';
  String _password = '';

  void _submit() {
    final form = _formKey.currentState!;
    if (form.validate()) {
      form.save();
      debugPrint('Email: $_email, Password: $_password');
      // Qui invieresti i dati al backend
    }
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          TextFormField(
            decoration: const InputDecoration(labelText: 'Email'),
            keyboardType: TextInputType.emailAddress,
            validator: (value) {
              if (value == null || value.isEmpty) {
                return 'Inserisci una email';
              }
              final regex = RegExp(r'^[\w.+-]+@[\w-]+\.[\w.-]+$');
              if (!regex.hasMatch(value)) {
                return 'Email non valida';
              }
              return null;
            },
            onSaved: (value) => _email = value ?? '',
          ),
          TextFormField(
            decoration: const InputDecoration(labelText: 'Password'),
            obscureText: true,
            validator: (value) {
              if (value == null || value.length < 6) {
                return 'La password deve avere almeno 6 caratteri';
              }
              return null;
            },
            onSaved: (value) => _password = value ?? '',
          ),
          const SizedBox(height: 16),
          ElevatedButton(
            onPressed: _submit,
            child: const Text('Accedi'),
          ),
        ],
      ),
    );
  }
}

Il flusso è semplice: validate() esegue tutti i validatori dei campi e mostra eventuali messaggi di errore; se restituisce true, save() invoca tutti i callback onSaved.

Strategie di validazione automatica

Di default la validazione avviene solo quando chiamiamo validate(). Possiamo però configurare quando i campi si auto-validano tramite la proprietà autovalidateMode:

Form(
  key: _formKey,
  autovalidateMode: AutovalidateMode.onUserInteraction,
  child: ...
)

I valori disponibili sono:

  • disabled (default): nessuna validazione automatica.
  • always: valida ad ogni rebuild, anche prima dell'interazione.
  • onUserInteraction: valida solo dopo che l'utente ha modificato il campo, l'opzione più gradita dal punto di vista UX.

Validatori riutilizzabili

Scrivere la logica di validazione inline diventa rapidamente ripetitivo. Conviene creare funzioni riutilizzabili e componibili:

class Validators {
  static String? required(String? value, [String message = 'Campo obbligatorio']) {
    if (value == null || value.trim().isEmpty) return message;
    return null;
  }

  static String? email(String? value) {
    if (value == null || value.isEmpty) return null;
    final regex = RegExp(r'^[\w.+-]+@[\w-]+\.[\w.-]+$');
    return regex.hasMatch(value) ? null : 'Email non valida';
  }

  static String? minLength(String? value, int length) {
    if (value != null && value.length < length) {
      return 'Minimo $length caratteri';
    }
    return null;
  }

  // Compone più validatori: si ferma al primo errore
  static FormFieldValidator<String> compose(
      List<FormFieldValidator<String>> validators) {
    return (value) {
      for (final validator in validators) {
        final result = validator(value);
        if (result != null) return result;
      }
      return null;
    };
  }
}

Utilizzo:

TextFormField(
  validator: Validators.compose([
    (v) => Validators.required(v),
    Validators.email,
  ]),
)

Gestire i controller

Quando hai bisogno di leggere o modificare il testo programmaticamente, usa un TextEditingController. Ricorda sempre di rilasciarlo nel dispose:

final _emailController = TextEditingController();

@override
void dispose() {
  _emailController.dispose();
  super.dispose();
}

Focus e navigazione tra i campi

Per migliorare l'esperienza utente puoi spostare il focus al campo successivo quando si preme "invio" sulla tastiera:

TextFormField(
  textInputAction: TextInputAction.next,
  onFieldSubmitted: (_) {
    FocusScope.of(context).nextFocus();
  },
)

Sull'ultimo campo puoi usare TextInputAction.done e invocare direttamente il submit.

Validazione asincrona

Un caso comune è verificare la disponibilità di uno username sul server. I validatori di TextFormField sono sincroni, quindi serve un approccio leggermente diverso: si conserva un messaggio di errore nello stato e si chiama setState dopo la richiesta.

String? _usernameError;

Future<void> _checkUsername(String value) async {
  final available = await api.isUsernameAvailable(value);
  setState(() {
    _usernameError = available ? null : 'Username già in uso';
  });
}

// Nel TextFormField
TextFormField(
  decoration: InputDecoration(
    labelText: 'Username',
    errorText: _usernameError,
  ),
  onChanged: (value) {
    // idealmente con debounce
    _checkUsername(value);
  },
)

È consigliabile aggiungere un meccanismo di debounce per non inondare il server di richieste ad ogni tasto premuto.

Best practice

  • Separa la logica di validazione in una classe dedicata e testabile.
  • Usa autovalidateMode: AutovalidateMode.onUserInteraction per un feedback immediato ma non invasivo.
  • Non dimenticare il dispose dei controller per evitare memory leak.
  • Valida sempre lato server: la validazione client-side è solo un aiuto all'utente, non una garanzia di sicurezza.
  • Considera pacchetti come flutter_form_builder quando hai form complessi con molti campi di tipi diversi.

Conclusione

Il sistema dei form di Flutter è semplice da apprendere ma sufficientemente flessibile per gestire scenari complessi. Partendo dal widget Form e dai TextFormField, e organizzando i validatori in modo riutilizzabile, puoi creare interfacce di inserimento dati robuste e con un'ottima esperienza utente. Con l'aggiunta della validazione asincrona e della gestione del focus, le tue schermate raggiungeranno la qualità delle migliori app native.