Forms are central to user interaction in applications. From login and signup pages to checkout processes and profile updates, forms collect critical information. But simply collecting input is not enough; developers must ensure that the information provided is valid, accurate, and meaningful. This is where custom validation rules come in.
Flutter provides robust support for building forms and validating input through the Form widget, FormField, and validator functions. However, the real power comes when you implement custom validation rules to handle specific needs such as validating emails, passwords, and phone numbers.
In this detailed guide, we will explore:
- Why custom validation is needed
- Email, password, and phone number validation rules
- Using regex for flexible validation
- Implementing real-time feedback for users
- Best practices and common pitfalls
By the end, you will be confident in implementing advanced, user-friendly validation in your Flutter applications.
Why Custom Validation is Needed
Built-in vs Custom Validation
Flutter’s TextFormField supports validation through a simple validator function. For example:
TextFormField(
validator: (value) {
if (value == null || value.isEmpty) {
return 'This field cannot be empty';
}
return null;
},
)
While this works for basic cases, most real-world applications require specific validation rules.
Scenarios Requiring Custom Validation
- Email format validation: Ensure input contains
@and a domain. - Password validation: Require minimum length, uppercase, digits, and special characters.
- Phone number validation: Enforce digit count and country-specific formats.
- Regex rules: Match complex patterns like postal codes or usernames.
- Real-time validation: Provide immediate feedback instead of waiting for form submission.
Without custom validation, applications risk collecting invalid or incomplete data, leading to poor user experience and possible system errors.
Email Validation
Why Email Validation Matters
Emails are one of the most common inputs in applications, used for login, registration, and communication. Collecting invalid emails can lead to:
- Failed authentication
- Lost communication
- Fake account creation
Simple Email Validation
String? validateEmail(String? value) {
if (value == null || value.isEmpty) {
return 'Please enter your email';
}
if (!value.contains('@')) {
return 'Invalid email: missing "@"';
}
return null;
}
Regex-Based Email Validation
For stricter validation, regex is used:
String? validateEmail(String? value) {
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (value == null || value.isEmpty) {
return 'Please enter your email';
}
if (!emailRegex.hasMatch(value)) {
return 'Enter a valid email address';
}
return null;
}
This ensures the input contains proper structure like [email protected].
Password Validation
Why Password Validation is Crucial
Weak passwords compromise security. Applications should enforce strong password rules to protect user accounts.
Basic Password Rule
String? validatePassword(String? value) {
if (value == null || value.isEmpty) {
return 'Please enter your password';
}
if (value.length < 6) {
return 'Password must be at least 6 characters';
}
return null;
}
Advanced Password Validation with Regex
Rules:
- At least 8 characters
- One uppercase letter
- One lowercase letter
- One number
- One special character
String? validatePassword(String? value) {
final passwordRegex =
RegExp(r'^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[@$!%*?&]).{8,}$');
if (value == null || value.isEmpty) {
return 'Please enter your password';
}
if (!passwordRegex.hasMatch(value)) {
return 'Password must include uppercase, lowercase, number, and special character';
}
return null;
}
Real-Life Use
This ensures passwords like P@ssw0rd123 are accepted, while weak ones like 12345 are rejected.
Phone Number Validation
Importance of Phone Validation
Phone numbers are used for:
- Account verification (via OTP)
- Contact information
- Delivery addresses
Invalid numbers create communication breakdowns.
Basic Validation
String? validatePhone(String? value) {
if (value == null || value.isEmpty) {
return 'Please enter your phone number';
}
if (value.length < 10) {
return 'Phone number must be at least 10 digits';
}
return null;
}
Regex-Based Phone Validation
For stricter formatting:
String? validatePhone(String? value) {
final phoneRegex = RegExp(r'^\+?[0-9]{10,13}$');
if (value == null || value.isEmpty) {
return 'Please enter your phone number';
}
if (!phoneRegex.hasMatch(value)) {
return 'Enter a valid phone number';
}
return null;
}
This allows optional country codes like +1, +91, and enforces 10–13 digits.
Regex-Based Validations
Why Use Regex
Regex (regular expressions) is a pattern-matching technique that makes complex validations simple. Instead of writing multiple conditions, regex allows concise rules.
Common Regex Patterns
- Email:
^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$ - Password:
^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[@$!%*?&]).{8,}$ - Phone:
^\+?[0-9]{10,13}$ - Username:
^[a-zA-Z0-9_]{3,16}$(3–16 characters, letters, numbers, underscores) - Postal Code:
^[0-9]{5}(?:-[0-9]{4})?$(US ZIP format)
Example: Username Validation
String? validateUsername(String? value) {
final usernameRegex = RegExp(r'^[a-zA-Z0-9_]{3,16}$');
if (value == null || value.isEmpty) {
return 'Please enter a username';
}
if (!usernameRegex.hasMatch(value)) {
return 'Username must be 3–16 characters, letters, numbers, or underscores';
}
return null;
}
Regex ensures concise, powerful validation rules.
Showing Real-Time Feedback
Validation is not only about correctness but also user experience. Instead of waiting until the form is submitted, apps should provide real-time feedback as users type.
Real-Time Email Feedback
TextFormField(
decoration: InputDecoration(labelText: 'Email'),
onChanged: (value) {
if (validateEmail(value) != null) {
print('Invalid email: $value');
} else {
print('Valid email: $value');
}
},
)
Using Controllers for Feedback
final emailController = TextEditingController();
@override
void initState() {
super.initState();
emailController.addListener(() {
final text = emailController.text;
if (validateEmail(text) != null) {
print('Invalid email');
} else {
print('Valid email');
}
});
}
Displaying Error Messages Dynamically
You can dynamically update error messages under the input field by setting errorText inside InputDecoration.
TextField(
decoration: InputDecoration(
labelText: 'Email',
errorText: validateEmail(emailText),
),
onChanged: (value) {
setState(() {
emailText = value;
});
},
)
This approach gives users immediate visual feedback, preventing frustration during form submission.
Complete Example: Signup Form with Custom Validations
class SignupForm extends StatefulWidget {
@override
_SignupFormState createState() => _SignupFormState();
}
class _SignupFormState extends State<SignupForm> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _phoneController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Signup')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _emailController,
decoration: InputDecoration(labelText: 'Email'),
validator: validateEmail,
),
TextFormField(
controller: _passwordController,
obscureText: true,
decoration: InputDecoration(labelText: 'Password'),
validator: validatePassword,
),
TextFormField(
controller: _phoneController,
decoration: InputDecoration(labelText: 'Phone'),
validator: validatePhone,
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
print('Email: ${_emailController.text}');
print('Password: ${_passwordController.text}');
print('Phone: ${_phoneController.text}');
}
},
child: Text('Register'),
),
],
),
),
),
);
}
String? validateEmail(String? value) {
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (value == null || value.isEmpty) {
return 'Please enter your email';
}
if (!emailRegex.hasMatch(value)) {
return 'Enter a valid email address';
}
return null;
}
String? validatePassword(String? value) {
final passwordRegex =
RegExp(r'^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[@$!%*?&]).{8,}$');
if (value == null || value.isEmpty) {
return 'Please enter your password';
}
if (!passwordRegex.hasMatch(value)) {
return 'Password must include uppercase, lowercase, number, and special character';
}
return null;
}
String? validatePhone(String? value) {
final phoneRegex = RegExp(r'^\+?[0-9]{10,13}$');
if (value == null || value.isEmpty) {
return 'Please enter your phone number';
}
if (!phoneRegex.hasMatch(value)) {
return 'Enter a valid phone number';
}
return null;
}
}
This form validates email, password, and phone number with regex and shows real-time feedback through validators.
Best Practices for Custom Validation
- Keep validation reusable
- Write functions for each validation rule and reuse across forms.
- Use regex carefully
- Test regex patterns to avoid false positives.
- Provide clear error messages
- Avoid technical jargon; use user-friendly text.
- Real-time feedback improves UX
- Validate on typing, not only on submission.
- Balance strictness and usability
- Overly strict rules frustrate users.
- Dispose controllers
- Prevent memory leaks by disposing controllers in
dispose().
- Prevent memory leaks by disposing controllers in
Common Mistakes with Validation
- Over-validating
- Example: Rejecting valid email formats due to overly strict regex.
- Weak password rules
- Accepting short or simple passwords.
- Ignoring localization
- Phone formats differ by country; make validation flexible.
- Not giving real-time feedback
- Forcing users to wait until submission causes frustration.
- Hardcoding error messages
- Instead, store messages in constants or localization files.
Leave a Reply