Flutter’s Form widget, combined with TextFormField and a GlobalKey<FormState>, gives you a built-in way to validate multiple fields together and show error messages — without wiring up validation state by hand for each field.
Basic Setup
class SignupForm extends StatefulWidget {
const SignupForm({super.key});
@override
State<SignupForm> createState() => _SignupFormState();
}
class _SignupFormState extends State<SignupForm> {
final _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
decoration: const InputDecoration(labelText: 'Email'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Email is required';
}
if (!value.contains('@')) {
return 'Enter a valid email';
}
return null;
},
),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
// all fields passed validation
}
},
child: const Text('Sign Up'),
),
],
),
);
}
}
Each TextFormField‘s validator returns null if the value is valid, or an error string to display if it isn’t. Calling _formKey.currentState!.validate() runs every field’s validator at once and returns true only if all of them passed.
Reading Values on Submit
Use a TextEditingController per field to read the entered values once validation passes:
final _emailController = TextEditingController();
TextFormField(
controller: _emailController,
validator: (value) => value!.contains('@') ? null : 'Enter a valid email',
)
// On submit, after validate() returns true:
final email = _emailController.text;
Remember to dispose() each controller in the State’s dispose() method to avoid leaking resources.
Validating as the User Types
By default, validators run when validate() is called explicitly (typically on submit). To show errors live as the user types, set autovalidateMode:
Form(
key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: /* ... */,
)
AutovalidateMode.onUserInteraction is usually the better choice over AutovalidateMode.always — it waits until the user has actually interacted with a field before showing an error, rather than showing “required” errors on every empty field the instant the form appears.
Cross-Field Validation
For validation that depends on more than one field — confirming a password matches — reference the other field’s controller from inside the validator:
TextFormField(
controller: _confirmPasswordController,
validator: (value) {
if (value != _passwordController.text) {
return 'Passwords do not match';
}
return null;
},
)