How do you handle form validation?

Medium PriorityAsked in ~55% of Flutter interviews

3 min read

Widgets & UI

Theory — the pieces

PieceWhat it does
FormContainer that coordinates a group of form fields
GlobalKey<FormState>Handle to call .validate(), .save(), .reset() from outside
TextFormFieldTextField + validator + onSaved hook
validator: (v) => v.isEmpty ? 'Required' : nullReturn error message, or null if valid
autovalidateModeWhen validators run — disabled, always, onUserInteraction, onUnfocus (3.19+)
TextEditingControllerRead/write the field's current value programmatically

Code in action

class LoginForm extends StatefulWidget {
  const LoginForm({super.key});
  @override
  State<LoginForm> createState() => _LoginFormState();
}

class _LoginFormState extends State<LoginForm> {
  final _formKey = GlobalKey<FormState>();
  final _email   = TextEditingController();
  final _pwd     = TextEditingController();

  @override
  void dispose() {
    _email.dispose();
    _pwd.dispose();
    super.dispose();
  }

  String? _emailRule(String? v) {
    if (v == null || v.isEmpty)       return 'Email is required';
    if (!v.contains('@'))             return 'Enter a valid email';
    return null;                                       // ✅ valid
  }

  String? _pwdRule(String? v) {
    if (v == null || v.isEmpty)       return 'Password is required';
    if (v.length < 8)                 return 'At least 8 characters';
    return null;
  }

  void _submit() {
    if (_formKey.currentState!.validate()) {
      // all good — submit
      api.login(_email.text, _pwd.text);
    }
  }

  @override
  Widget build(BuildContext context) => Form(
    key: _formKey,
    child: Column(children: [
      TextFormField(
        controller: _email,
        decoration: const InputDecoration(labelText: 'Email'),
        keyboardType: TextInputType.emailAddress,
        autovalidateMode: AutovalidateMode.onUserInteraction,
        validator: _emailRule,
      ),
      const SizedBox(height: 16),
      TextFormField(
        controller: _pwd,
        decoration: const InputDecoration(labelText: 'Password'),
        obscureText: true,
        autovalidateMode: AutovalidateMode.onUserInteraction,
        validator: _pwdRule,
      ),
      const SizedBox(height: 24),
      ElevatedButton(onPressed: _submit, child: const Text('Log in')),
    ]),
  );
}

When to use what

SituationApproach
Simple form (login, signup, settings)Form + TextFormField + validator
Async validation (check email exists)Disable button while loading; validate manually before submit
Complex domain validation across fieldsValidate at submit time with custom logic, not per-field
Reactive validation (validate on every keystroke)autovalidateMode: AutovalidateMode.always
Large form / nested sectionsSplit into multiple Forms, validate each, or use a form package (reactive_forms, flutter_form_builder)

Common mistakes to avoid

// ❌ Validators that read controllers instead of the passed value
validator: (_) => _email.text.isEmpty ? 'required' : null;
// ✅ Use the parameter — it stays in sync with the field
validator: (v) => v!.isEmpty ? 'required' : null;

// ❌ Forgetting to dispose controllers → memory leak
// ✅ override dispose() and call ctrl.dispose() for each

// ❌ Validating only on submit and ignoring inline UX
// ✅ autovalidateMode: AutovalidateMode.onUserInteraction is usually right

// ❌ Trying to do async work inside `validator`
validator: (v) async { ... }                       // ❌ must return String?, not Future
// ✅ Run the async check on submit and call setState with the error message

// ❌ Mixing controller and onChanged for the same field
// Pick one — controller for read/programmatic edits, onChanged for value-only state

// ❌ Showing errors before the user touches anything
// ✅ AutovalidateMode.onUserInteraction (Flutter > 2.10) does the right thing

Interview follow-ups

  1. How do you handle async validation (e.g., "is this email taken")? Validators must be synchronous (return String?, not a Future). Run the async check separately — typically on submit, while disabling the button and showing a spinner. If it fails, surface the error via setState (or by reusing a field-level error variable that your validator returns).

  2. What's the difference between onChanged, onSaved, and controller? onChanged fires every keystroke. onSaved runs only when you call _formKey.currentState!.save() — useful for collecting form values at submit time. A TextEditingController lets you read/write the value programmatically and survives rebuilds.

  3. When would you use multiple Forms on one screen? For independent sections (e.g., "address" and "payment") where you want them validated separately, or for multi-step wizards. Each has its own GlobalKey<FormState> and validates in isolation.

  4. What does autovalidateMode: AutovalidateMode.onUserInteraction actually do? Errors don't show until the field has been touched or the form has been submitted once. This avoids the bad UX of yelling "Email required" the moment the screen opens.


How helpful was this content?

Please sign in to rate this article.