Lesson 10 / 25

Forms and Validation

Form, TextFormField and controllers.

Collecting text safely

A TextField shows an editable text box; a TextEditingController reads or sets its text and must be disposed. Wrap several TextFormFields in a Form with a GlobalKey<FormState>: each field has a validator that returns an error string or null, and formKey.currentState!.validate() runs them all and shows messages. onSaved plus save() collects values. autovalidateMode controls when errors appear, keyboardType and textInputAction tune the keyboard, obscureText hides passwords, and FocusNode lets you move focus between fields.

Interact and look consistent

Forms collect and validate input, gesture widgets react to touches, and a theme keeps colours and text consistent.

Three ideas: forms and validation, buttons and gestures, Material 3 theming.
Figure 4.1 — Input, validation, feedback and theme.

A sign-in form

Validation with a form key.

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

class _SignInFormState extends State<SignInForm> {
  final _formKey = GlobalKey<FormState>();
  final _email = TextEditingController();

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

  void _submit() {
    if (_formKey.currentState!.validate()) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Signing in ${_email.text}')),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(children: [
        TextFormField(
          controller: _email,
          decoration: const InputDecoration(labelText: 'Email'),
          keyboardType: TextInputType.emailAddress,
          validator: (v) =>
              (v == null || !v.contains('@')) ? 'Enter a valid email' : null,
        ),
        TextFormField(
          decoration: const InputDecoration(labelText: 'Password'),
          obscureText: true,
          validator: (v) =>
              (v == null || v.length < 8) ? 'At least 8 characters' : null,
        ),
        FilledButton(onPressed: _submit, child: const Text('Sign in')),
      ]),
    );
  }
}

Validate on the server too

Client-side validation improves feedback, but anyone can call your API directly. Repeat important checks on the backend.

Quick check: What should a TextFormField validator return when the value is valid?

  • null
  • An empty string
  • true
  • The value itself
Answer

null — Any non-null string is treated as an error message.