Popular Searches
Popular Course Categories
Popular Courses

Form Validation in Flutter

Form Validation in Flutter

Flutter Forms & User Input

Form Validation in Flutter – Detailed Notes

Form validation is the process of checking whether the information entered by a user is correct, complete, and acceptable before the application processes or submits it. Flutter provides built-in form validation through widgets such as Form, TextFormField, FormField, validators, and FormState.

A typical Flutter validation flow is: create a Form, assign a GlobalKey, add form fields with validator functions, and call validate() when the user submits the form. Flutter's official validation recipe follows this same pattern. :contentReference[oaicite:0]{index=0}


1. What Is Form Validation?

Form validation ensures that the data entered by a user satisfies predefined rules.

For example:

  • Name should not be empty.
  • Email should contain a valid email format.
  • Password should meet the minimum length requirement.
  • Phone number should contain an appropriate number of digits.
  • Confirm password should match the password.
  • A required dropdown should have a selected value.
  • Required terms and conditions should be accepted.

Example

TextFormField(
  validator: (value) {
    if (value == null || value.trim().isEmpty) {
      return 'Name is required';
    }
    return null;
  },
)

If the input is invalid, the validator returns an error message. If the input is valid, it returns null. This is the standard Flutter FormField validation pattern. :contentReference[oaicite:1]{index=1}


2. Why Is Form Validation Important?

  • Prevents incomplete form submissions.
  • Improves user experience.
  • Helps maintain consistent data.
  • Provides immediate feedback about incorrect input.
  • Reduces invalid data sent to APIs or databases.
  • Helps users understand what information is required.
  • Improves the reliability of registration, login, checkout, profile, and contact forms.

3. Main Flutter Widgets Used for Validation

Widget/Class Purpose
Form Groups multiple form fields and manages form state.
TextFormField Provides text input integrated with Form validation.
FormField Base widget for creating validated form fields.
GlobalKey Provides access to the FormState.
FormState Provides operations such as validate(), save(), and reset().
validator Contains validation rules for a field.
AutovalidateMode Controls when automatic validation occurs.
TextEditingController Provides programmatic access to text input.

TextFormField integrates a text field with the enclosing Form, which allows validation and other FormField functionality. :contentReference[oaicite:2]{index=2}


4. Basic Form Validation Structure

A basic validation implementation consists of three main parts:

  1. Create a Form.
  2. Add TextFormField widgets with validators.
  3. Call validate() when the user submits the form.
final _formKey = GlobalKey();

Form(
  key: _formKey,
  child: Column(
    children: [
      TextFormField(
        validator: (value) {
          if (value == null || value.isEmpty) {
            return 'This field is required';
          }
          return null;
        },
      ),
      ElevatedButton(
        onPressed: () {
          if (_formKey.currentState!.validate()) {
            print('Form is valid');
          }
        },
        child: const Text('Submit'),
      ),
    ],
  ),
)

5. Creating a GlobalKey

Create a form key inside the State class:

final _formKey = GlobalKey();

Assign it to the Form widget:

Form(
  key: _formKey,
  child: Column(
    children: [
      // Form fields
    ],
  ),
)

The key allows the application to access the form's state and call methods such as validate(). Flutter's official documentation recommends using a GlobalKey to access a form when appropriate. :contentReference[oaicite:3]{index=3}


6. Understanding the validator Property

The validator property contains a function that receives the current field value.

validator: (value) {
  if (value == null || value.isEmpty) {
    return 'Please enter a value';
  }
  return null;
}

The validator has two possible outcomes:

Return Value Meaning
Error message The field is invalid and the message is displayed.
null The field is valid.

Flutter's FormField.validator API specifies that an error string is returned for invalid input and null is returned when there is no validation error. :contentReference[oaicite:4]{index=4}


7. Required Field Validation

The most common validation rule is checking whether a field is empty.

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;
  },
)

Why Use trim()?

trim() removes unnecessary spaces from the beginning and end of the input.

String name = value.trim();

8. Email Validation

Email validation checks whether the user has entered an acceptable email format.

Simple Email Validation

TextFormField(
  keyboardType: TextInputType.emailAddress,
  decoration: const InputDecoration(
    labelText: 'Email Address',
    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) {
  if (value == null || value.trim().isEmpty) {
    return 'Email is required';
  }
  if (!isValidEmail(value.trim())) {
    return 'Enter a valid email address';
  }
  return null;
}

9. Password Validation

Password validation is commonly used in login and registration forms.

Minimum Length Validation

TextFormField(
  obscureText: true,
  decoration: const InputDecoration(
    labelText: 'Password',
    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;
  },
)

Strong Password Validation

validator: (value) {
  if (value == null || value.isEmpty) {
    return 'Password is required';
  }

  if (value.length < 8) {
    return 'Password must contain at least 8 characters';
  }

  if (!RegExp(r'[A-Z]').hasMatch(value)) {
    return 'Password must contain an uppercase letter';
  }

  if (!RegExp(r'[a-z]').hasMatch(value)) {
    return 'Password must contain a lowercase letter';
  }

  if (!RegExp(r'[0-9]').hasMatch(value)) {
    return 'Password must contain a number';
  }

  return null;
}

10. Confirm Password Validation

Confirm password validation ensures that the second password matches the first 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;
  },
)

11. Phone Number Validation

Phone validation can check whether the field contains a suitable number of digits.

TextFormField(
  keyboardType: TextInputType.phone,
  decoration: const InputDecoration(
    labelText: 'Phone Number',
    border: OutlineInputBorder(),
  ),
  validator: (value) {
    if (value == null || value.trim().isEmpty) {
      return 'Phone number is required';
    }

    if (!RegExp(r'^\d{10}$').hasMatch(value.trim())) {
      return 'Enter a valid 10-digit phone number';
    }

    return null;
  },
)

The exact phone validation rules should be chosen according to the application's supported countries and requirements.


12. Username Validation

TextFormField(
  decoration: const InputDecoration(
    labelText: 'Username',
    border: OutlineInputBorder(),
  ),
  validator: (value) {
    if (value == null || value.trim().isEmpty) {
      return 'Username is required';
    }

    if (value.trim().length < 4) {
      return 'Username must contain at least 4 characters';
    }

    if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value.trim())) {
      return 'Only letters, numbers, and underscores are allowed';
    }

    return null;
  },
)

13. Name Validation

TextFormField(
  decoration: const InputDecoration(
    labelText: 'Name',
    border: OutlineInputBorder(),
  ),
  validator: (value) {
    if (value == null || value.trim().isEmpty) {
      return 'Name is required';
    }

    if (value.trim().length < 2) {
      return 'Name must contain at least 2 characters';
    }

    return null;
  },
)

14. Number Validation

TextFormField(
  keyboardType: TextInputType.number,
  decoration: const InputDecoration(
    labelText: 'Age',
    border: OutlineInputBorder(),
  ),
  validator: (value) {
    if (value == null || value.trim().isEmpty) {
      return 'Age is required';
    }

    final age = int.tryParse(value.trim());

    if (age == null) {
      return 'Enter a valid number';
    }

    if (age < 18) {
      return 'Age must be 18 or above';
    }

    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) {
  if (value == null || value.isEmpty) {
    return 'This field is required';
  }

  if (value.length < 5 || value.length > 50) {
    return 'Enter between 5 and 50 characters';
  }

  return null;
}

16. Validating Dropdown Fields

Dropdown 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;
  },
)

17. Validating Checkbox Input

Checkboxes can be 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 submission:

if (!acceptedTerms) {
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(
      content: Text(
        'Please accept the Terms and Conditions',
      ),
    ),
  );
  return;
}

18. Validating Radio Button Selection

String? gender;

if (gender == null) {
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(
      content: Text('Please select a gender'),
    ),
  );
  return;
}

Radio buttons generally use application state to determine whether an option has been selected, while text-based fields can use a validator.


19. FormState.validate()

The validate() method validates every descendant FormField and returns true when there are no errors. If there are errors, it returns false and the form displays the validation results. :contentReference[oaicite:5]{index=5}

if (_formKey.currentState!.validate()) {
  print('All fields are valid');
} else {
  print('Form contains errors');
}

20. Form Validation Flow

User enters data
      ↓
User taps Submit
      ↓
FormState.validate()
      ↓
Validators execute
      ↓
Are there errors?
   ↙           ↘
 Yes            No
 ↓              ↓
Show errors     Process data
 ↓              ↓
User corrects   Submit/save
input

21. Complete Login Form 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 submitLogin() {
    if (_formKey.currentState!.validate()) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text('Login data 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) {
                  if (value == null || value.trim().isEmpty) {
                    return 'Email is required';
                  }

                  if (!value.contains('@')) {
                    return 'Enter a valid email address';
                  }

                  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: submitLogin,
                  child: const Text('Login'),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

22. Complete Registration Form Validation

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,
          child: Column(
            children: [
              TextFormField(
                controller: nameController,
                decoration: const InputDecoration(
                  labelText: 'Full Name',
                  border: OutlineInputBorder(),
                ),
                validator: (value) {
                  if (value == null || value.trim().isEmpty) {
                    return 'Full name is required';
                  }
                  return null;
                },
              ),
              const SizedBox(height: 16),
              TextFormField(
                controller: emailController,
                keyboardType: TextInputType.emailAddress,
                decoration: const InputDecoration(
                  labelText: 'Email',
                  border: OutlineInputBorder(),
                ),
                validator: (value) {
                  if (value == null || value.trim().isEmpty) {
                    return 'Email is required';
                  }

                  if (!value.contains('@')) {
                    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) {
                  if (value == null || value.trim().isEmpty) {
                    return 'Phone number is required';
                  }

                  if (!RegExp(r'^\d{10}$')
                      .hasMatch(value.trim())) {
                    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) {
                  if (value == null || value.isEmpty) {
                    return 'Password is required';
                  }

                  if (value.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'),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

23. AutovalidateMode

AutovalidateMode controls when a Form or FormField automatically runs validation. Current Flutter APIs include modes such as disabled, always, onUserInteraction, onUnfocus, and onUserInteractionIfError. :contentReference[oaicite:6]{index=6}

Disabled

Form(
  key: _formKey,
  autovalidateMode: AutovalidateMode.disabled,
  child: ...
)

Automatic validation is disabled.

Always

Form(
  key: _formKey,
  autovalidateMode: AutovalidateMode.always,
  child: ...
)

Validation runs automatically, even without user interaction.

On User Interaction

Form(
  key: _formKey,
  autovalidateMode: AutovalidateMode.onUserInteraction,
  child: ...
)

Validation occurs after user interaction with the form fields.

On Unfocus

Form(
  key: _formKey,
  autovalidateMode: AutovalidateMode.onUnfocus,
  child: ...
)

Validation occurs when the field loses focus.

On User Interaction If Error

Form(
  key: _formKey,
  autovalidateMode: AutovalidateMode.onUserInteractionIfError,
  child: ...
)

This mode validates after user interaction when the field already has an error. :contentReference[oaicite:7]{index=7}


24. Field-Level Autovalidation

Autovalidation can also be configured directly on a form field.

TextFormField(
  autovalidateMode: AutovalidateMode.onUserInteraction,
  validator: (value) {
    if (value == null || value.isEmpty) {
      return 'Email is required';
    }
    return null;
  },
)

The FormField API supports autovalidateMode for controlling automatic validation and error text updates. :contentReference[oaicite:8]{index=8}


25. Validation with TextEditingController

A controller is useful when one field's validation depends on the value of another field.

final passwordController = TextEditingController();

TextFormField(
  controller: passwordController,
  obscureText: true,
  validator: (value) {
    if (value == null || value.length < 8) {
      return 'Minimum 8 characters required';
    }
    return null;
  },
)

TextFormField(
  obscureText: true,
  validator: (value) {
    if (value != passwordController.text) {
      return 'Passwords do not match';
    }
    return null;
  },
)

When controllers are created manually, they should be disposed in the State object's dispose() method. Flutter's official documentation recommends disposing controllers when they are no longer needed. :contentReference[oaicite:9]{index=9}


26. Form Validation with onSaved

The onSaved callback can be used to store values after validation.

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);
}

27. Resetting Validation Errors

Calling reset() resets the form fields to their initial values and resets their validation state.

_formKey.currentState!.reset();

Example:

OutlinedButton(
  onPressed: () {
    _formKey.currentState!.reset();
  },
  child: const Text('Reset'),
)

28. Focus Management After Validation

When validation fails, an application can move focus to a specific field.

final emailFocusNode = FocusNode();

TextFormField(
  focusNode: emailFocusNode,
  decoration: const InputDecoration(
    labelText: 'Email',
  ),
)

Focus can be requested programmatically:

FocusScope.of(context).requestFocus(emailFocusNode);

Focus nodes are long-lived objects and should be managed by a State object when manually created. :contentReference[oaicite:10]{index=10}


29. Showing Validation Messages

The simplest way to show field-level errors is to return a message from the validator.

validator: (value) {
  if (value == null || value.isEmpty) {
    return 'Please enter your email';
  }
  return null;
}

Flutter's TextFormField uses the validation result to display the field's error state. :contentReference[oaicite:11]{index=11}


30. Improving Error Messages

Validation messages should be clear and actionable.

Weak Message Better Message
Invalid Enter a valid email address
Error Password must contain at least 8 characters
Required Please enter your full name
Wrong Passwords do not match
Select Please select your role

31. Validation and Keyboard Types

Using the appropriate keyboard type improves the input experience.

TextFormField(
  keyboardType: TextInputType.emailAddress,
)

TextFormField(
  keyboardType: TextInputType.phone,
)

TextFormField(
  keyboardType: TextInputType.number,
)

TextFormField(
  keyboardType: TextInputType.multiline,
)

32. Validation with Input Formatting

Input formatters can restrict what users enter before validation occurs.

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;
  },
)

This approach can reduce invalid characters while the validator checks whether the final value meets the application's rules.


33. Preventing Empty Spaces

When validating text, trim whitespace before checking the value.

validator: (value) {
  final text = value?.trim() ?? '';

  if (text.isEmpty) {
    return 'This field is required';
  }

  return null;
}

34. Validation with Multiple Rules

A single validator can contain several rules.

validator: (value) {
  final password = value ?? '';

  if (password.isEmpty) {
    return 'Password is required';
  }

  if (password.length < 8) {
    return 'Minimum 8 characters required';
  }

  if (!password.contains(RegExp(r'[A-Z]'))) {
    return 'Include at least one uppercase letter';
  }

  if (!password.contains(RegExp(r'[0-9]'))) {
    return 'Include at least one number';
  }

  return null;
}

35. Reusable Validation Functions

When an application has many forms, repeated validation logic can be moved into reusable functions.

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');
  },
)

Email Validator

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;
}

36. Creating a Validation Utility Class

For larger projects, validators can be organized in a separate 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) {
    if (value == null || value.isEmpty) {
      return 'Password is required';
    }

    if (value.length < 8) {
      return 'Minimum 8 characters required';
    }

    return null;
  }
}

Usage:

TextFormField(
  validator: Validators.email,
)

37. Complete Form Validation Workflow

Form
│
├── Name Field
│   └── Required validation
│
├── Email Field
│   └── Email validation
│
├── Phone Field
│   └── Phone validation
│
├── Password Field
│   └── Password validation
│
├── Confirm Password Field
│   └── Password matching
│
├── Role Field
│   └── Selection validation
│
└── Submit
    └── FormState.validate()
        ├── Invalid → Show errors
        └── Valid → Process data

38. Complete Practical Form Validation Example

import 'package:flutter/material.dart';

class UserForm extends StatefulWidget {
  const UserForm({super.key});

  @override
  State createState() => _UserFormState();
}

class _UserFormState extends State {
  final _formKey = GlobalKey();

  final nameController = TextEditingController();
  final emailController = TextEditingController();
  final passwordController = TextEditingController();
  final confirmPasswordController = TextEditingController();

  @override
  void dispose() {
    nameController.dispose();
    emailController.dispose();
    passwordController.dispose();
    confirmPasswordController.dispose();
    super.dispose();
  }

  void submitForm() {
    if (!_formKey.currentState!.validate()) {
      return;
    }

    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(
        content: Text('All form fields are valid'),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Form Validation'),
      ),
      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',
                  prefixIcon: Icon(Icons.person),
                  border: OutlineInputBorder(),
                ),
                validator: (value) {
                  if (value == null ||
                      value.trim().isEmpty) {
                    return 'Full name is required';
                  }

                  if (value.trim().length < 2) {
                    return 'Enter a valid name';
                  }

                  return null;
                },
              ),
              const SizedBox(height: 16),
              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: 16),
              TextFormField(
                controller: confirmPasswordController,
                obscureText: true,
                decoration: const InputDecoration(
                  labelText: 'Confirm Password',
                  prefixIcon: Icon(Icons.lock_outline),
                  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: 20),
              SizedBox(
                width: double.infinity,
                child: ElevatedButton(
                  onPressed: submitForm,
                  child: const Text('Submit'),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

39. Validation Before API Submission

Client-side validation should happen before sending form data 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 an API here.
}

Client-side validation improves the user experience, but applications should still validate data on the server when data is sent to a backend.


40. Loading State During Validation and Submission

For asynchronous submissions, use a loading state to prevent repeated requests.

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('Form submitted successfully'),
      ),
    );
  } finally {
    if (mounted) {
      setState(() {
        isLoading = false;
      });
    }
  }
}

41. Common Form Validation Mistakes

1. Creating the GlobalKey Inside build()

// Avoid
Widget build(BuildContext context) {
  final formKey = GlobalKey();
  ...
}

Create the key as a field of the State class instead:

final _formKey = GlobalKey();

Flutter's official form validation recipe specifically creates and stores the key outside build(). :contentReference[oaicite:12]{index=12}

2. Forgetting to Call validate()

// Incorrect
onPressed: () {
  submitData();
}

Better:

onPressed: () {
  if (_formKey.currentState!.validate()) {
    submitData();
  }
}

3. Returning a Message for Valid Input

// Incorrect
validator: (value) {
  if (value != null && value.isNotEmpty) {
    return 'Valid';
  }
  return null;
}

A validator should return an error message only when invalid and null when valid.

4. Forgetting to Dispose Controllers

@override
void dispose() {
  emailController.dispose();
  passwordController.dispose();
  super.dispose();
}

5. Using Only UI Validation

Client-side validation is useful for user feedback, but sensitive or important data should also be validated by the backend.


42. Best Practices for Form Validation

  • Use a Form to group related fields.
  • Use GlobalKey to access form state when appropriate.
  • Use TextFormField for text fields that need Form integration.
  • Return clear and useful error messages.
  • Use trim() when appropriate for text fields.
  • Use suitable keyboard types.
  • Use input formatters where appropriate.
  • Keep validation rules reusable.
  • Use AutovalidateMode according to the desired user experience.
  • Do not recreate the form key inside build().
  • Dispose manually created controllers and focus nodes.
  • Validate before submitting data.
  • Prevent duplicate submissions during asynchronous operations.
  • Keep client-side validation and backend validation separate.
  • Use scrollable layouts for long forms.
  • Test validation with empty, invalid, boundary, and valid values.

43. Form Validation Testing Checklist

Test Case Expected Result
Empty required field Required-field error appears.
Invalid email Email validation error appears.
Valid email No email error appears.
Short password Password length error appears.
Matching passwords Confirmation validation succeeds.
Different passwords Password mismatch error appears.
Invalid phone Phone validation error appears.
Valid phone No phone error appears.
No dropdown selection Selection error appears.
Valid form Submission processing starts.
Reset button Form returns to its initial state.

44. Form Validation vs Manual Validation

Form Validation Manual Validation
Uses Form and FormField. Uses custom application logic.
Validators are attached to fields. Rules may be checked manually.
Errors integrate with form fields. Errors require custom UI handling.
validate() checks descendant fields. Developer controls the complete process.
Convenient for standard Flutter forms. Useful for specialized validation workflows.

45. Interview Questions

Q1. What is form validation in Flutter?

Form validation is the process of checking user input against predefined rules before processing or submitting the data.

Q2. Which widget is commonly used for validated text input?

TextFormField is commonly used because it integrates text input with the surrounding Form. :contentReference[oaicite:13]{index=13}

Q3. What does validator return?

It returns an error message when the input is invalid and null when the input is valid.

Q4. How do you validate an entire form?

if (_formKey.currentState!.validate()) {
  // Form is valid
}

Q5. What does validate() return?

It returns true when all descendant form fields are valid and false when one or more fields have validation errors. :contentReference[oaicite:14]{index=14}

Q6. What is AutovalidateMode?

AutovalidateMode controls when Form and FormField widgets automatically perform validation.

Q7. Why use TextEditingController?

It allows the application to read, modify, and listen to text entered into a text field.

Q8. Why should TextEditingController be disposed?

Controllers should be disposed when no longer needed to clean up resources. :contentReference[oaicite:15]{index=15}


46. Quick Revision

  • Form groups form fields.
  • TextFormField integrates text input with form validation.
  • GlobalKey provides access to the form state.
  • validator contains field validation rules.
  • Return an error message for invalid input.
  • Return null for valid input.
  • validate() validates the form.
  • save() executes onSaved callbacks.
  • reset() resets form fields and validation state.
  • AutovalidateMode controls automatic validation.
  • Use controllers when programmatic access to input is required.
  • Dispose manually created controllers.
  • Validate data before submission.
  • Use server-side validation in addition to client-side validation for backend data.

47. Official Flutter Resources


48. JustAcademy Flutter Course Resources


49. Key Takeaways

  • Form validation checks whether user-entered data satisfies application rules.
  • Flutter provides built-in validation through Form, FormField, and TextFormField.
  • A GlobalKey can be used to access the form state.
  • The validator function should return an error message for invalid input and null for valid input.
  • FormState.validate() validates the descendant form fields and returns whether the form is valid.
  • Validation can be applied to names, emails, passwords, phone numbers, dropdowns, and other user inputs.
  • AutovalidateMode provides control over automatic validation behavior.
  • Reusable validation functions help keep larger Flutter projects clean and maintainable.
  • Controllers should be disposed when they are no longer needed.
  • Client-side validation should be combined with appropriate backend validation when submitting data to a server.
Previous Next
whatsapp