Validating Email, Password, and Other Inputs in Flutter – Detailed Notes
Input validation is an important part of Flutter application development. It checks whether information entered by a user follows the required rules before the application processes, stores, or submits that information.
Flutter provides Form, TextFormField, FormField, validator, FormState, and AutovalidateMode to build validation workflows. A validator returns an error message when input is invalid and null when the input is valid. The form can then be checked using FormState.validate(). Flutter Official Form Validation Guide
1. What Is Input Validation?
Input validation is the process of checking user-entered data against predefined rules.
For example:
- Name should not be empty.
- Email should have an acceptable email format.
- Password should meet the application's security requirements.
- Confirm password should match the password.
- Phone number should contain the required number of digits.
- Age should be within an acceptable range.
- A required dropdown should have a selected value.
- A required checkbox should be selected.
- Address should not be empty.
- Username should contain only allowed characters.
2. Why Validate User Input?
- Prevents incomplete data submission.
- Provides useful feedback to users.
- Reduces invalid data sent to a backend.
- Improves application reliability.
- Improves the overall user experience.
- Helps users correct mistakes before submission.
- Ensures required fields contain appropriate values.
Flutter's official form validation workflow uses a Form, a GlobalKey, field validators, and validate() to determine whether form input is valid. Read the official Flutter validation recipe
3. Main Widgets Used for Input Validation
| Widget/Class |
Purpose |
Form |
Groups multiple form fields. |
TextFormField |
Creates a text field that integrates with Form validation. |
FormField |
Provides the base functionality for validated form fields. |
GlobalKey |
Provides access to the form state. |
FormState.validate() |
Runs validation for the form fields. |
validator |
Contains the validation rules for a field. |
AutovalidateMode |
Controls automatic validation behavior. |
TextEditingController |
Reads and controls text input. |
inputFormatters |
Can restrict or transform input before validation. |
TextFormField wraps a text field and integrates it with the enclosing Form, providing validation and other FormField functionality. Flutter TextFormField Documentation
4. Basic Validation Structure
The basic validation structure contains a Form, one or more fields, validators, and a submit button.
final _formKey = GlobalKey();
Form(
key: _formKey,
child: Column(
children: [
TextFormField(
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'This field is required';
}
return null;
},
),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
print('Form is valid');
}
},
child: const Text('Submit'),
),
],
),
)
5. Understanding the validator Function
The validator function receives the current field value and checks it against validation rules.
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'This field is required';
}
return null;
}
| Validator Result |
Meaning |
| Error message |
The input is invalid and the error is displayed. |
null |
The input is considered valid. |
The Flutter API specifies that the validator returns an error string for invalid input or null when there is no validation error. FormField validator API
6. Required Field Validation
Required-field validation checks whether the user has entered a value.
TextFormField(
decoration: const InputDecoration(
labelText: 'Full Name',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Please enter your full name';
}
return null;
},
)
Using trim()
final name = value?.trim() ?? '';
if (name.isEmpty) {
return 'Name is required';
}
Using trim() is useful when spaces should not be treated as meaningful input at the beginning or end of a field.
7. Email Validation
Email validation checks whether the entered value follows an acceptable email pattern.
Simple Email Validation
TextFormField(
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'Email Address',
prefixIcon: Icon(Icons.email),
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Email is required';
}
if (!value.contains('@')) {
return 'Enter a valid email address';
}
return null;
},
)
Regular Expression Email Validation
bool isValidEmail(String email) {
return RegExp(
r'^[^@\s]+@[^@\s]+\.[^@\s]+$',
).hasMatch(email);
}
Use the function inside a validator:
validator: (value) {
final email = value?.trim() ?? '';
if (email.isEmpty) {
return 'Email is required';
}
if (!isValidEmail(email)) {
return 'Enter a valid email address';
}
return null;
}
Email Examples
8. Password Validation
Password validation checks whether the password satisfies the application's required rules.
Minimum Password Length
TextFormField(
obscureText: true,
decoration: const InputDecoration(
labelText: 'Password',
prefixIcon: Icon(Icons.lock),
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Password is required';
}
if (value.length < 8) {
return 'Password must contain at least 8 characters';
}
return null;
},
)
Password with Multiple Rules
validator: (value) {
final password = value ?? '';
if (password.isEmpty) {
return 'Password is required';
}
if (password.length < 8) {
return 'Minimum 8 characters required';
}
if (!RegExp(r'[A-Z]').hasMatch(password)) {
return 'Include at least one uppercase letter';
}
if (!RegExp(r'[a-z]').hasMatch(password)) {
return 'Include at least one lowercase letter';
}
if (!RegExp(r'[0-9]').hasMatch(password)) {
return 'Include at least one number';
}
return null;
}
Password Validation Rules
| Rule |
Example |
| Required |
Password cannot be empty. |
| Minimum length |
At least 8 characters. |
| Uppercase |
At least one A-Z character. |
| Lowercase |
At least one a-z character. |
| Number |
At least one digit. |
| Special character |
At least one symbol when required by the application's policy. |
9. Confirm Password Validation
Confirm password validation ensures that the password entered in the confirmation field matches the original password.
final passwordController = TextEditingController();
TextFormField(
controller: passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Password',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextFormField(
obscureText: true,
decoration: const InputDecoration(
labelText: 'Confirm Password',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please confirm your password';
}
if (value != passwordController.text) {
return 'Passwords do not match';
}
return null;
},
)
10. Username Validation
A username validator can restrict the minimum length and allowed characters.
TextFormField(
decoration: const InputDecoration(
labelText: 'Username',
prefixIcon: Icon(Icons.person),
border: OutlineInputBorder(),
),
validator: (value) {
final username = value?.trim() ?? '';
if (username.isEmpty) {
return 'Username is required';
}
if (username.length < 4) {
return 'Username must contain at least 4 characters';
}
if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(username)) {
return 'Use only letters, numbers, and underscores';
}
return null;
},
)
11. Name Validation
TextFormField(
decoration: const InputDecoration(
labelText: 'Full Name',
border: OutlineInputBorder(),
),
validator: (value) {
final name = value?.trim() ?? '';
if (name.isEmpty) {
return 'Name is required';
}
if (name.length < 2) {
return 'Name must contain at least 2 characters';
}
return null;
},
)
12. Phone Number Validation
Phone validation depends on the application's supported regions and expected format. For a simple 10-digit example:
TextFormField(
keyboardType: TextInputType.phone,
decoration: const InputDecoration(
labelText: 'Phone Number',
prefixIcon: Icon(Icons.phone),
border: OutlineInputBorder(),
),
validator: (value) {
final phone = value?.trim() ?? '';
if (phone.isEmpty) {
return 'Phone number is required';
}
if (!RegExp(r'^\d{10}$').hasMatch(phone)) {
return 'Enter a valid 10-digit phone number';
}
return null;
},
)
Example Inputs
| Input |
Result |
| 9876543210 |
Valid for this example |
| 12345 |
Invalid |
| 98765abc10 |
Invalid |
| Empty |
Invalid |
13. Age Validation
Age validation can use int.tryParse() to safely convert text to an integer.
TextFormField(
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: 'Age',
border: OutlineInputBorder(),
),
validator: (value) {
final text = value?.trim() ?? '';
if (text.isEmpty) {
return 'Age is required';
}
final age = int.tryParse(text);
if (age == null) {
return 'Enter a valid number';
}
if (age < 18) {
return 'Age must be 18 or above';
}
if (age > 120) {
return 'Enter a valid age';
}
return null;
},
)
14. Number Validation
TextFormField(
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: 'Quantity',
border: OutlineInputBorder(),
),
validator: (value) {
final quantity = int.tryParse(value?.trim() ?? '');
if (quantity == null) {
return 'Enter a valid number';
}
if (quantity <= 0) {
return 'Quantity must be greater than zero';
}
return null;
},
)
15. Minimum and Maximum Length Validation
Minimum Length
validator: (value) {
if (value == null || value.isEmpty) {
return 'This field is required';
}
if (value.length < 5) {
return 'Minimum 5 characters required';
}
return null;
}
Maximum Length
validator: (value) {
if (value == null || value.isEmpty) {
return 'This field is required';
}
if (value.length > 50) {
return 'Maximum 50 characters allowed';
}
return null;
}
Both Minimum and Maximum
validator: (value) {
final text = value?.trim() ?? '';
if (text.isEmpty) {
return 'This field is required';
}
if (text.length < 5 || text.length > 50) {
return 'Enter between 5 and 50 characters';
}
return null;
}
16. URL Validation
URL validation can be useful for website, portfolio, social media, or resource fields.
TextFormField(
keyboardType: TextInputType.url,
decoration: const InputDecoration(
labelText: 'Website',
border: OutlineInputBorder(),
),
validator: (value) {
final url = value?.trim() ?? '';
if (url.isEmpty) {
return 'Website URL is required';
}
final uri = Uri.tryParse(url);
if (uri == null ||
!uri.hasScheme ||
!uri.hasAuthority) {
return 'Enter a valid URL';
}
return null;
},
)
17. Address Validation
TextFormField(
maxLines: 4,
decoration: const InputDecoration(
labelText: 'Address',
alignLabelWithHint: true,
border: OutlineInputBorder(),
),
validator: (value) {
final address = value?.trim() ?? '';
if (address.isEmpty) {
return 'Address is required';
}
if (address.length < 10) {
return 'Please enter a complete address';
}
return null;
},
)
18. Pincode Validation
For an Indian 6-digit pincode example:
TextFormField(
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: 'Pincode',
border: OutlineInputBorder(),
),
validator: (value) {
final pincode = value?.trim() ?? '';
if (pincode.isEmpty) {
return 'Pincode is required';
}
if (!RegExp(r'^[1-9][0-9]{5}$').hasMatch(pincode)) {
return 'Enter a valid 6-digit pincode';
}
return null;
},
)
19. Dropdown Validation
Selection fields can also participate in form validation.
String? selectedRole;
DropdownButtonFormField(
value: selectedRole,
decoration: const InputDecoration(
labelText: 'Select Role',
border: OutlineInputBorder(),
),
items: const [
DropdownMenuItem(
value: 'student',
child: Text('Student'),
),
DropdownMenuItem(
value: 'developer',
child: Text('Developer'),
),
DropdownMenuItem(
value: 'designer',
child: Text('Designer'),
),
],
onChanged: (value) {
setState(() {
selectedRole = value;
});
},
validator: (value) {
if (value == null) {
return 'Please select a role';
}
return null;
},
)
Flutter's FormField system is also used by form-integrated selection widgets. Flutter FormField API
20. Checkbox Validation
Checkboxes are often used for required agreements such as terms and conditions.
bool acceptedTerms = false;
CheckboxListTile(
title: const Text(
'I accept the Terms and Conditions',
),
value: acceptedTerms,
onChanged: (value) {
setState(() {
acceptedTerms = value ?? false;
});
},
)
Before submitting:
if (!acceptedTerms) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Please accept the Terms and Conditions',
),
),
);
return;
}
21. Radio Button Validation
Radio buttons can be validated by checking whether an option has been selected.
String? selectedGender;
RadioListTile(
title: const Text('Male'),
value: 'male',
groupValue: selectedGender,
onChanged: (value) {
setState(() {
selectedGender = value;
});
},
)
Before submission:
if (selectedGender == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Please select an option'),
),
);
return;
}
22. Date Validation
Date validation can be used for date of birth, appointment dates, registration dates, and other date-based inputs.
DateTime? selectedDate;
Future selectDate() async {
final pickedDate = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(1950),
lastDate: DateTime.now(),
);
if (pickedDate != null) {
setState(() {
selectedDate = pickedDate;
});
}
}
Validate the selected date before submission:
if (selectedDate == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Please select a date'),
),
);
return;
}
23. TextEditingController for Validation
TextEditingController can be used when validation depends on the value of another field or when the application needs direct access to entered text.
final emailController = TextEditingController();
TextFormField(
controller: emailController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'Email',
),
)
Read the value:
final email = emailController.text.trim();
Controllers created manually should be disposed when they are no longer needed. Flutter TextEditingController Guide
@override
void dispose() {
emailController.dispose();
super.dispose();
}
24. Cross-Field Validation
Cross-field validation means validating one field using the value of another field.
A common example is password confirmation.
final passwordController = TextEditingController();
final confirmPasswordController = TextEditingController();
TextFormField(
controller: passwordController,
obscureText: true,
validator: (value) {
if (value == null || value.length < 8) {
return 'Password must contain at least 8 characters';
}
return null;
},
),
TextFormField(
controller: confirmPasswordController,
obscureText: true,
validator: (value) {
if (value != passwordController.text) {
return 'Passwords do not match';
}
return null;
},
)
25. Real-Time Validation with onChanged
The onChanged callback can be used to react whenever the user changes the field value.
TextFormField(
onChanged: (value) {
print('Current value: $value');
},
)
This can be useful for password strength indicators, character counters, search suggestions, and other interactive experiences. Flutter's documentation provides both onChanged and TextEditingController approaches for handling text changes. Flutter Text Field Changes Guide
26. Password Strength Indicator
A password strength indicator can provide visual feedback while the user types.
String password = '';
TextFormField(
obscureText: true,
onChanged: (value) {
setState(() {
password = value;
});
},
decoration: const InputDecoration(
labelText: 'Password',
border: OutlineInputBorder(),
),
)
Text(
password.length >= 8
? 'Password length requirement met'
: 'Use at least 8 characters',
)
For a more advanced implementation, the application can separately check length, uppercase characters, lowercase characters, numbers, and special characters.
27. Input Formatting
Input formatters can restrict or transform user input before it reaches the validation logic.
import 'package:flutter/services.dart';
TextFormField(
keyboardType: TextInputType.phone,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
validator: (value) {
if (value == null || value.length != 10) {
return 'Enter a 10-digit phone number';
}
return null;
},
)
For example, FilteringTextInputFormatter.digitsOnly can restrict a field to digits while the validator checks whether the resulting value meets the required length.
28. AutovalidateMode
AutovalidateMode controls when form fields automatically perform validation.
Disabled
TextFormField(
autovalidateMode: AutovalidateMode.disabled,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Required';
}
return null;
},
)
Automatic validation is disabled.
On User Interaction
TextFormField(
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Required';
}
return null;
},
)
The field automatically validates after user interaction.
Always
TextFormField(
autovalidateMode: AutovalidateMode.always,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Required';
}
return null;
},
)
The field is automatically validated even without user interaction.
On Unfocus
TextFormField(
autovalidateMode: AutovalidateMode.onUnfocus,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Required';
}
return null;
},
)
The field validates when it loses focus.
On User Interaction If Error
TextFormField(
autovalidateMode:
AutovalidateMode.onUserInteractionIfError,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Required';
}
return null;
},
)
Current Flutter provides these validation modes through the AutovalidateMode enum. AutovalidateMode API
29. FormState.validate()
The validate() method runs the validators for the form's descendant fields.
if (_formKey.currentState!.validate()) {
print('All inputs are valid');
} else {
print('Some inputs are invalid');
}
When validation finds errors, Flutter rebuilds the form to display the corresponding error messages. Flutter Form Validation
30. FormState.save()
After successful validation, save() can be used to execute the onSaved callback for form fields.
String email = '';
TextFormField(
decoration: const InputDecoration(
labelText: 'Email',
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Email is required';
}
return null;
},
onSaved: (value) {
email = value ?? '';
},
)
Then:
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
print(email);
}
31. Form Reset
The reset() method resets form fields to their initial values and resets their validation state.
OutlinedButton(
onPressed: () {
_formKey.currentState!.reset();
},
child: const Text('Reset'),
)
32. Focus Management After Validation
Focus can be moved to a particular field when needed, such as after detecting a validation problem.
final emailFocusNode = FocusNode();
TextFormField(
focusNode: emailFocusNode,
decoration: const InputDecoration(
labelText: 'Email',
),
)
Request focus:
FocusScope.of(context).requestFocus(emailFocusNode);
Manually created focus nodes should be disposed when no longer needed. Flutter Focus and Text Fields
33. Reusable Email Validator
Reusable validators help prevent duplicated validation code across multiple screens.
String? validateEmail(String? value) {
final email = value?.trim() ?? '';
if (email.isEmpty) {
return 'Email is required';
}
if (!RegExp(
r'^[^@\s]+@[^@\s]+\.[^@\s]+$',
).hasMatch(email)) {
return 'Enter a valid email address';
}
return null;
}
Use it like this:
TextFormField(
validator: validateEmail,
)
34. Reusable Password Validator
String? validatePassword(String? value) {
final password = value ?? '';
if (password.isEmpty) {
return 'Password is required';
}
if (password.length < 8) {
return 'Password must contain at least 8 characters';
}
if (!RegExp(r'[A-Z]').hasMatch(password)) {
return 'Include at least one uppercase letter';
}
if (!RegExp(r'[a-z]').hasMatch(password)) {
return 'Include at least one lowercase letter';
}
if (!RegExp(r'[0-9]').hasMatch(password)) {
return 'Include at least one number';
}
return null;
}
Use it like this:
TextFormField(
obscureText: true,
validator: validatePassword,
)
35. Reusable Phone Validator
String? validatePhone(String? value) {
final phone = value?.trim() ?? '';
if (phone.isEmpty) {
return 'Phone number is required';
}
if (!RegExp(r'^\d{10}$').hasMatch(phone)) {
return 'Enter a valid 10-digit phone number';
}
return null;
}
36. Reusable Required Field Validator
String? validateRequired(
String? value,
String fieldName,
) {
if (value == null || value.trim().isEmpty) {
return '$fieldName is required';
}
return null;
}
Usage:
TextFormField(
validator: (value) {
return validateRequired(value, 'Full Name');
},
)
37. Creating a Validator Utility Class
class Validators {
static String? required(
String? value,
String fieldName,
) {
if (value == null || value.trim().isEmpty) {
return '$fieldName is required';
}
return null;
}
static String? email(String? value) {
final email = value?.trim() ?? '';
if (email.isEmpty) {
return 'Email is required';
}
if (!RegExp(
r'^[^@\s]+@[^@\s]+\.[^@\s]+$',
).hasMatch(email)) {
return 'Enter a valid email address';
}
return null;
}
static String? password(String? value) {
final password = value ?? '';
if (password.isEmpty) {
return 'Password is required';
}
if (password.length < 8) {
return 'Minimum 8 characters required';
}
return null;
}
static String? phone(String? value) {
final phone = value?.trim() ?? '';
if (phone.isEmpty) {
return 'Phone number is required';
}
if (!RegExp(r'^\d{10}$').hasMatch(phone)) {
return 'Enter a valid 10-digit phone number';
}
return null;
}
}
Usage
TextFormField(
validator: Validators.email,
)
TextFormField(
obscureText: true,
validator: Validators.password,
)
TextFormField(
keyboardType: TextInputType.phone,
validator: Validators.phone,
)
38. Complete Login Validation Example
import 'package:flutter/material.dart';
class LoginForm extends StatefulWidget {
const LoginForm({super.key});
@override
State createState() => _LoginFormState();
}
class _LoginFormState extends State {
final _formKey = GlobalKey();
final emailController = TextEditingController();
final passwordController = TextEditingController();
@override
void dispose() {
emailController.dispose();
passwordController.dispose();
super.dispose();
}
void login() {
if (!_formKey.currentState!.validate()) {
return;
}
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Login input is valid'),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Login'),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: emailController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'Email',
prefixIcon: Icon(Icons.email),
border: OutlineInputBorder(),
),
validator: (value) {
final email = value?.trim() ?? '';
if (email.isEmpty) {
return 'Email is required';
}
if (!RegExp(
r'^[^@\s]+@[^@\s]+\.[^@\s]+$',
).hasMatch(email)) {
return 'Enter a valid email';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Password',
prefixIcon: Icon(Icons.lock),
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Password is required';
}
if (value.length < 8) {
return 'Minimum 8 characters required';
}
return null;
},
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: login,
child: const Text('Login'),
),
),
],
),
),
),
);
}
}
39. Complete Registration Validation Example
import 'package:flutter/material.dart';
class RegistrationForm extends StatefulWidget {
const RegistrationForm({super.key});
@override
State createState() => _RegistrationFormState();
}
class _RegistrationFormState extends State {
final _formKey = GlobalKey();
final nameController = TextEditingController();
final emailController = TextEditingController();
final phoneController = TextEditingController();
final passwordController = TextEditingController();
final confirmPasswordController = TextEditingController();
String? selectedRole;
bool acceptedTerms = false;
@override
void dispose() {
nameController.dispose();
emailController.dispose();
phoneController.dispose();
passwordController.dispose();
confirmPasswordController.dispose();
super.dispose();
}
void submitForm() {
if (!_formKey.currentState!.validate()) {
return;
}
if (selectedRole == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Please select a role'),
),
);
return;
}
if (!acceptedTerms) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Please accept the terms'),
),
);
return;
}
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Registration successful'),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Registration'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
autovalidateMode:
AutovalidateMode.onUserInteraction,
child: Column(
children: [
TextFormField(
controller: nameController,
decoration: const InputDecoration(
labelText: 'Full Name',
border: OutlineInputBorder(),
),
validator: (value) {
final name = value?.trim() ?? '';
if (name.isEmpty) {
return 'Full name is required';
}
if (name.length < 2) {
return 'Enter a valid name';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: emailController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
validator: (value) {
final email = value?.trim() ?? '';
if (email.isEmpty) {
return 'Email is required';
}
if (!RegExp(
r'^[^@\s]+@[^@\s]+\.[^@\s]+$',
).hasMatch(email)) {
return 'Enter a valid email';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: phoneController,
keyboardType: TextInputType.phone,
decoration: const InputDecoration(
labelText: 'Phone Number',
border: OutlineInputBorder(),
),
validator: (value) {
final phone = value?.trim() ?? '';
if (phone.isEmpty) {
return 'Phone number is required';
}
if (!RegExp(r'^\d{10}$')
.hasMatch(phone)) {
return 'Enter a valid 10-digit phone number';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Password',
border: OutlineInputBorder(),
),
validator: (value) {
final password = value ?? '';
if (password.isEmpty) {
return 'Password is required';
}
if (password.length < 8) {
return 'Minimum 8 characters required';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: confirmPasswordController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Confirm Password',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Confirm password is required';
}
if (value != passwordController.text) {
return 'Passwords do not match';
}
return null;
},
),
const SizedBox(height: 16),
DropdownButtonFormField(
value: selectedRole,
decoration: const InputDecoration(
labelText: 'Role',
border: OutlineInputBorder(),
),
items: const [
DropdownMenuItem(
value: 'student',
child: Text('Student'),
),
DropdownMenuItem(
value: 'developer',
child: Text('Developer'),
),
DropdownMenuItem(
value: 'designer',
child: Text('Designer'),
),
],
onChanged: (value) {
setState(() {
selectedRole = value;
});
},
validator: (value) {
if (value == null) {
return 'Please select a role';
}
return null;
},
),
const SizedBox(height: 8),
CheckboxListTile(
contentPadding: EdgeInsets.zero,
title: const Text(
'I accept the Terms and Conditions',
),
value: acceptedTerms,
onChanged: (value) {
setState(() {
acceptedTerms = value ?? false;
});
},
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: submitForm,
child: const Text('Register'),
),
),
],
),
),
),
);
}
}
40. Validation Error Messages
Good error messages should explain what the user needs to correct.
| Input |
Example Error Message |
| Empty name |
Please enter your full name |
| Invalid email |
Enter a valid email address |
| Short password |
Password must contain at least 8 characters |
| Password mismatch |
Passwords do not match |
| Invalid phone |
Enter a valid phone number |
| Invalid username |
Use only letters, numbers, and underscores |
| No dropdown selection |
Please select a role |
| Empty address |
Address is required |
41. Validation Before API Submission
Always validate form input before sending it to an API.
Future submitData() async {
if (!_formKey.currentState!.validate()) {
return;
}
final data = {
'name': nameController.text.trim(),
'email': emailController.text.trim(),
'password': passwordController.text,
};
print(data);
// Send data to your API here.
}
Client-side validation improves user feedback, but applications should also perform appropriate validation on the backend before storing or processing submitted data.
42. Loading State During Submission
When validation succeeds and the application starts an asynchronous operation, use a loading state to prevent repeated submissions.
bool isLoading = false;
Future submitForm() async {
if (!_formKey.currentState!.validate()) {
return;
}
setState(() {
isLoading = true;
});
try {
await Future.delayed(
const Duration(seconds: 2),
);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Data submitted successfully'),
),
);
} finally {
if (mounted) {
setState(() {
isLoading = false;
});
}
}
}
Submit Button
ElevatedButton(
onPressed: isLoading ? null : submitForm,
child: isLoading
? const CircularProgressIndicator()
: const Text('Submit'),
)
43. Common Validation Mistakes
1. Creating GlobalKey Inside build()
// Avoid
Widget build(BuildContext context) {
final formKey = GlobalKey();
...
}
Create the key once in the State object:
final _formKey = GlobalKey();
2. Forgetting validate()
// Incorrect
onPressed: () {
submitData();
}
Use:
onPressed: () {
if (_formKey.currentState!.validate()) {
submitData();
}
}
3. Returning an Error for Valid Input
// Incorrect
validator: (value) {
if (value != null && value.isNotEmpty) {
return 'Valid';
}
return null;
}
A valid field should return null.
4. Forgetting to Dispose Controllers
@override
void dispose() {
emailController.dispose();
passwordController.dispose();
super.dispose();
}
5. Using Weak Validation Rules
Validation rules should match the actual requirements of the application instead of assuming that one generic rule is appropriate for every project.
6. Depending Only on Client-Side Validation
Client-side validation should not replace backend validation when data is sent to a server.
44. Best Practices for Email Validation
- Use
TextInputType.emailAddress.
- Trim unnecessary whitespace.
- Check for an acceptable email structure.
- Provide a clear error message.
- Do not make unnecessarily restrictive assumptions about valid email addresses.
- Perform appropriate server-side validation when the email is submitted to a backend.
45. Best Practices for Password Validation
- Use
obscureText: true for password fields.
- Define password rules clearly.
- Show useful validation feedback.
- Validate password confirmation separately.
- Do not log passwords in production applications.
- Do not store plaintext passwords in application databases.
- Use secure authentication and password handling on the backend.
46. Best Practices for Other Inputs
- Use the correct keyboard type.
- Use input formatters where they provide useful input restrictions.
- Validate required fields.
- Validate numeric ranges when applicable.
- Validate selection fields.
- Use clear error messages.
- Use reusable validation functions for repeated rules.
- Test valid, invalid, empty, and boundary values.
47. Input Validation Testing Checklist
| Test |
Expected Result |
| Empty required field |
Required-field error appears. |
| Valid email |
Email validation succeeds. |
| Invalid email |
Email error appears. |
| Short password |
Password length error appears. |
| Strong password |
Password validation succeeds according to the defined rules. |
| Different confirmation password |
Password mismatch error appears. |
| Matching confirmation password |
Confirmation validation succeeds. |
| Invalid phone |
Phone error appears. |
| Valid phone |
Phone validation succeeds. |
| Invalid number |
Number error appears. |
| Valid number |
Number validation succeeds. |
| No dropdown selection |
Selection error appears. |
| Valid form |
Submission logic executes. |
| Reset form |
Fields and validation state reset. |
48. Validation Flow
User enters input
↓
Input is changed
↓
Optional automatic validation
↓
User taps Submit
↓
FormState.validate()
↓
Validators execute
↓
Input valid?
↙ ↘
No Yes
↓ ↓
Show errors Process data
↓ ↓
Correct input API/Database
submission
49. Quick Revision
Form groups multiple form fields.
TextFormField integrates text input with form validation.
validator contains validation rules.
- Return an error string when input is invalid.
- Return
null when input is valid.
GlobalKey provides access to the form state.
validate() runs form validation.
save() executes onSaved callbacks.
reset() resets the form state.
AutovalidateMode controls automatic validation.
- Email, password, phone, name, username, URL, number, address, and selection fields can all have application-specific validation rules.
- Cross-field validation is useful for matching fields such as password and confirm password.
- Reusable validator functions help keep code clean.
- Client-side validation should be complemented by appropriate server-side validation.
50. Official Flutter Resources
51. JustAcademy Flutter Course Resources
52. Key Takeaways
- Input validation ensures that user-entered information follows the application's rules.
- Flutter provides built-in validation through
Form, FormField, and TextFormField.
- Email validation checks for an acceptable email structure.
- Password validation can check length and other defined requirements.
- Confirm password validation compares two related fields.
- Phone, username, name, age, number, URL, address, and pincode fields can use specialized validators.
- Dropdowns and other FormField-based controls can participate in validation.
AutovalidateMode can control when validation occurs automatically.
- Reusable validation functions reduce duplicate code.
- Controllers should be disposed when manually created.
- Validate data before sending it to an API.
- Server-side validation remains important for backend applications.