# Forms and Validation — Flutter

Source: https://www.skillbyai.com/en/flutter/i-forms

> 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 **TextFormField**s 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.](assets/figures/flutter/section-4-map.svg) — Figure 4.1 — Input, validation, feedback and theme.

## A sign-in form

Validation with a form key.

```dart
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.

**Quiz:** What should a TextFormField validator return when the value is valid?

- [x] null
- [ ] An empty string
- [ ] true
- [ ] The value itself

*Answer:* null. Any non-null string is treated as an error message.
