Mastering Form Validation in Flutter
Mastering Form Validation in Flutter
What You’ll Learn
Learn how to build robust forms with real-time validation using Flutter’s built-in Form and TextFormField widgets. This is essential for login screens, registration forms, and any user input.
Why Use Form Over TextField?
While TextField handles basic input, Form gives you:
- Centralized validation logic
- Easy form-wide validation triggers
- Built-in error display
- Clean state management with
GlobalKey
Example: Login Form with Validation
import 'package:flutter/material.dart';
class LoginForm extends StatefulWidget {
@override
State<LoginForm> createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _isLoading = false;
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
String? _validateEmail(String? value) {
if (value == null || value.isEmpty) {
return 'Email is required';
}
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegex.hasMatch(value)) {
return 'Enter a valid email';
}
return null; // null means validation passed
}
String? _validatePassword(String? value) {
if (value == null || value.isEmpty) {
return 'Password is required';
}
if (value.length < 6) {
return 'Password must be at least 6 characters';
}
return null;
}
Future<void> _submitForm() async {
// Validate all fields
if (_formKey.currentState!.validate()) {
setState(() => _isLoading = true);
// Simulate API call
await Future.delayed(Duration(seconds: 2));
setState(() => _isLoading = false);
// Show success message
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Login successful!')),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Login')),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Form(
key: _formKey, // Connect form to the GlobalKey
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextFormField(
controller: _emailController,
decoration: InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.email),
),
keyboardType: TextInputType.emailAddress,
validator: _validateEmail,
),
SizedBox(height: 16),
TextFormField(
controller: _passwordController,
decoration: InputDecoration(
labelText: 'Password',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.lock),
),
obscureText: true,
validator: _validatePassword,
),
SizedBox(height: 24),
SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton(
onPressed: _isLoading ? null : _submitForm,
child: _isLoading
? CircularProgressIndicator(color: Colors.white)
: Text('Login'),
),
),
],
),
),
),
);
}
}
How It Works
- GlobalKey: Links your state to the form widget
- validator: Function that returns
nullif valid, error message if invalid - validate(): Runs all validators and shows errors automatically
- TextEditingController: Gets the current input value
Advanced: Real-Time Validation
Add autovalidateMode for instant feedback:
Form(
key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: // ... your fields
)
Modes:
disabled: Only validate on submit (default)onUserInteraction: Validate after first interactionalways: Validate on every change
Try It Yourself
Enhance the form with:
- “Confirm Password” field that checks if passwords match
- Phone number field with format validation (e.g., XXX-XXX-XXXX)
- A checkbox for “Terms & Conditions” that must be checked
- Custom error styling with
InputDecoration.errorStyle
Challenge: Create a registration form with username, email, password, and password confirmation. Show a green checkmark icon next to fields that pass validation.
Tip of the Day
Validation Performance: Keep validators simple and synchronous. For async validation (like checking if an email exists), use a separate button or debounced API call—don’t put async logic directly in validators.
Quick Debug: Use _formKey.currentState?.save() to collect all field values at once. Each TextFormField can have an onSaved callback that captures its value into your data model.