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 comevalidate()osave().TextFormField: una versione diTextFieldintegrata 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.onUserInteractionper un feedback immediato ma non invasivo. - Non dimenticare il
disposedei 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_builderquando 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.
