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:

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

  1. GlobalKey: Links your state to the form widget
  2. validator: Function that returns null if valid, error message if invalid
  3. validate(): Runs all validators and shows errors automatically
  4. 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:

Try It Yourself

Enhance the form with:

  1. “Confirm Password” field that checks if passwords match
  2. Phone number field with format validation (e.g., XXX-XXX-XXXX)
  3. A checkbox for “Terms & Conditions” that must be checked
  4. 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.