An form is a set of fields that collects and validates data entered by the user.
In the previous article, we learned how to use TextField for user input. However, this data can be incomplete or incorrectly formatted.
Users write emails without @, leave fields empty, or type letters in the “Age” field. If we send this “dirty” data to our server, everything will fail.
To catch this before submission, Flutter provides the Form widget and its validators. Client-side validation improves the user experience, but the server must always validate the data as well.
The Form widget
Form is an invisible container. By itself, it doesn’t render anything, but it serves to group multiple inputs and validate them all at once.
To use it, we need two special ingredients:
- Replace our
TextFieldwidgets withTextFormField. - Create a Global Key (
GlobalKey) to control the form from outside.
TextField and TextFormField
Until now, we’ve been using TextField. Inside a form, we’ll use its bigger sibling: TextFormField.
They look identical, but TextFormField has one extra, very important property: validator.
The validator logic
The validator property is a function that receives the text written by the user and must return:
null: If the data is CORRECT."Error text": If the data is INCORRECT. Flutter will display this text in red below the input.
TextFormField(
decoration: InputDecoration(labelText: "Name"),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please write something'; // ❌ Error
}
return null; // ✅ All good
},
)
Controlling the form with GlobalKey<FormState>
Here comes the technical part. How does the “Submit” button know it needs to check the inputs that are higher up in the code?
We need a “remote control” that connects the button to the form. That remote is the GlobalKey.
class MyForm extends StatefulWidget {
@override
_MyFormState createState() => _MyFormState();
}
class _MyFormState extends State<MyForm> {
// 1. Create a unique key for this form
final _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Form(
key: _formKey, // 2. Assign the key to the Form widget
child: Column(
children: [
TextFormField(
validator: (value) {
if (value!.isEmpty) return 'Required field';
return null;
},
),
ElevatedButton(
onPressed: () {
// 3. Use the key to validate
if (_formKey.currentState!.validate()) {
// If it returns true, everything is OK
print("Processing data...");
}
},
child: Text("Submit"),
)
],
),
);
}
}
What does .validate() do?
When you call _formKey.currentState!.validate(), Flutter iterates over all the TextFormField widgets inside that Form, executes their validator functions one by one and:
- If any fails, it displays the error in red and returns
false. - If all return
null, it returnstrue.
Format validation
Validating that a field is not empty is easy. But how do we validate a real email? This is where Regular Expressions (Regex) come in.
Dart has the RegExp class to help us.
TextFormField(
decoration: InputDecoration(labelText: "Email"),
keyboardType: TextInputType.emailAddress,
validator: (value) {
// 1. Validate that it's not empty
if (value == null || value.isEmpty) return 'Required';
// 2. Validate the format with Regex
// Basic structural check, not verifying if the account exists
String pattern = r'^[^\s@]+@[^\s@]+\.[^\s@]+$';
RegExp regex = RegExp(pattern);
if (!regex.hasMatch(value)) {
return 'Enter a valid email';
}
return null;
},
)
The r prefix before the string (r'...') indicates it’s a “Raw String”. It is necessary for writing Regex in Dart without having to escape all backslashes.
Auto-validation
By default, errors only appear when you press the “Submit” button. Sometimes it’s better to warn the user as they type.
We can change this behavior with autovalidateMode.
Form(
key: _formKey,
// Validates as soon as the user interacts with the field
autovalidateMode: AutovalidateMode.onUserInteraction,
child: ...
)
This greatly improves the user experience, as the red message disappears the instant the user corrects the error, without having to press “Submit” again.