Forms and validation

Flutter · Practical · 12 min

Sign-up, checkout and settings screens all need validated input with clear error messages.

A `Form` groups TextFormFields (and other FormFields) so you can validate and save them together. Give the form a `GlobalKey<FormState>` stored in a State object, so you can reach the form's state from a button.

Each TextFormField takes a `validator`: a function that receives the current value (String?) and returns an error message, or null when valid. On submit:

if (_formKey.currentState!.validate()) {
  _formKey.currentState!.save();
  // send data
}

validate() runs every validator, shows the messages under the fields and returns whether all passed. onSaved callbacks then copy values into your model when you call save().

UX details:

  • autovalidateMode: AutovalidateMode.onUserInteraction validates each field after the user first edits it, instead of only on submit.
  • Use keyboardType: TextInputType.emailAddress, textInputAction: TextInputAction.next and autofillHints for faster input.
  • Disable the submit button while a request is in flight and show progress.
  • Server-side errors (e.g. "email already taken") can be shown by storing them in state and returning them from the validator, or via InputDecoration.errorText.

Keep validation logic in plain Dart functions so it's easy to unit test. Wrap the form in a SingleChildScrollView so the keyboard doesn't hide fields.

Example

import 'package:flutter/material.dart';
String? validateEmail(String? v) =>
    (v == null || !RegExp(r'^\S+@\S+\.\S+$').hasMatch(v)) ? 'Enter a valid email' : null;
class SignUpForm extends StatefulWidget {
  const SignUpForm({super.key});
  @override
  State<SignUpForm> createState() => _SignUpFormState();
}
class _SignUpFormState extends State<SignUpForm> {
  final _formKey = GlobalKey<FormState>();
  String _email = '';
  @override
  Widget build(BuildContext context) => Form(
        key: _formKey, autovalidateMode: AutovalidateMode.onUserInteraction,
        child: Column(children: [
          TextFormField(decoration: const InputDecoration(labelText: 'Email'),
              validator: validateEmail, onSaved: (v) => _email = v!.trim()),
          TextFormField(obscureText: true, decoration: const InputDecoration(labelText: 'Password'),
              validator: (v) => (v ?? '').length < 8 ? 'At least 8 characters' : null),
          FilledButton(child: const Text('Sign up'), onPressed: () {
            if (_formKey.currentState!.validate()) { _formKey.currentState!.save(); debugPrint('ok $_email'); }
          }),
        ]),
      );
}

Remember

  • A GlobalKey<FormState> lets you call validate() and save()
  • Validators return an error string or null
  • Keep validation in plain functions you can unit test

Watch out for

  • Creating the GlobalKey inside build, which makes a new form state on every rebuild.

Check yourself

  1. What should a TextFormField validator return when the input is valid?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.flutter.dev/cookbook/forms/validation

Builds on

Unlocks

Nothing builds on this yet.