Updated June 2026. Tested on Flutter 3.x and Dart 3. Part of the Techalyst Flutter series.
Sign-in, sign-up, checkout, settings. Almost every app collects input, which means almost every app needs forms with validation that does not feel clumsy. Flutter has a tidy pattern for this built around three pieces: a Form, the fields inside it, and a key that ties them together. Once you have wired it once, every form after looks the same.
TextField or TextFormField
Flutter has two text inputs and the difference is simple. TextField is the raw input with no validation hooks. TextFormField is the same field wired to work inside a Form, with a built-in slot for a validator and an error message. For anything you need to validate, use TextFormField.
The Form and its key
You wrap your fields in a Form and give it a GlobalKey<FormState>. That key is your handle to the whole form, so you can validate or save every field at once from the submit button.
final _formKey = GlobalKey<FormState>();
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; // null means valid
},
),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
// every field passed, go ahead and submit
}
},
child: const Text('Submit'),
),
],
),
)
The whole validation model lives in those validator functions. Each returns an error string when the value is wrong, or null when it is fine. Calling _formKey.currentState!.validate() runs every field's validator, shows the error messages under the ones that failed, and returns true only if they all passed. One call validates the entire form.
Reading what the user typed
You have two ways to get the values out. The common one is a TextEditingController, which gives you the field's text any time you want it:
final _emailController = TextEditingController();
TextFormField(controller: _emailController, ...);
// on submit
final email = _emailController.text;
Controllers hold resources, so dispose them when the widget is gone:
@override
void dispose() {
_emailController.dispose();
super.dispose();
}
The other way is onSaved on each field plus _formKey.currentState!.save(), which collects values into your own variables. Controllers are usually simpler, and they let you read or change the field at any moment, not just on submit.
Validating as they type
By default a form validates only when you call validate(), which is good for a submit button. If you want errors to appear live as the user types, set autovalidateMode:
Form(
key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: ...,
)
onUserInteraction is the friendly choice, it waits until a field has been touched before nagging, instead of showing errors on a pristine empty form.
Wrapping up
A Flutter form is a Form with a GlobalKey, fields built from TextFormField, and a validator on each one that returns an error string or null. Validate everything with a single validate() call on submit, read values through a TextEditingController that you remember to dispose, and switch on autovalidateMode: onUserInteraction when you want live feedback. That same skeleton handles every form you will build, from a one-field search to a full checkout.
All comments ()
No comments yet
Be the first to leave a comment on this post.