iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Forms & Validation

Form + FormField + a GlobalKey<FormState> coordinate validation across fields. TextFormField is the daily driver; onSaved collects values; validator returns an error string or null.

Form, validators, save, focus chain

EXAMPLE
import 'package:flutter/material.dart';

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

class _SignupFormState extends State<SignupForm> {
    final _formKey = GlobalKey<FormState>();
    final _emailFocus = FocusNode();
    final _pwFocus    = FocusNode();
    final _data = <String, String>{};
    bool _showPw = false;
    bool _loading = false;

    @@override
    void dispose() {
        _emailFocus.dispose();
        _pwFocus.dispose();
        super.dispose();
    }

    Future<void> _submit() async {
        FocusScope.of(context).unfocus();
        if (!_formKey.currentState!.validate()) return;
        _formKey.currentState!.save();
        setState(() => _loading = true);
        try {
            await api.signup(_data['email']!, _data['password']!);
            if (mounted) Navigator.pushReplacementNamed(context, '/');
        } catch (e) {
            if (mounted) ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(content: Text('Signup failed: $e')),
            );
        } finally {
            if (mounted) setState(() => _loading = false);
        }
    }

    @@override
    Widget build(BuildContext context) {
        return Form(
            key: _formKey,
            autovalidateMode: AutovalidateMode.onUserInteraction,
            child: Column(
                children: [
                    // 1) Name
                    TextFormField(
                        decoration: const InputDecoration(
                            labelText: 'Name',
                            border: OutlineInputBorder(),
                        ),
                        textInputAction: TextInputAction.next,
                        onFieldSubmitted: (_) => _emailFocus.requestFocus(),
                        validator: (v) =>
                            v == null || v.trim().isEmpty ? 'Please enter a name' : null,
                        onSaved: (v) => _data['name'] = v!.trim(),
                    ),
                    const SizedBox(height: 12),

                    // 2) Email
                    TextFormField(
                        focusNode: _emailFocus,
                        decoration: const InputDecoration(
                            labelText: 'Email',
                            border: OutlineInputBorder(),
                        ),
                        keyboardType: TextInputType.emailAddress,
                        autocorrect: false,
                        textCapitalization: TextCapitalization.none,
                        textInputAction: TextInputAction.next,
                        onFieldSubmitted: (_) => _pwFocus.requestFocus(),
                        validator: (v) {
                            if (v == null || v.isEmpty) return 'Please enter an email';
                            if (!RegExp(r'^[^@@\s]+@@[^@@\s]+\.[^@@\s]+$').hasMatch(v)) return 'Invalid email';
                            return null;
                        },
                        onSaved: (v) => _data['email'] = v!.trim(),
                    ),
                    const SizedBox(height: 12),

                    // 3) Password — with show/hide
                    TextFormField(
                        focusNode: _pwFocus,
                        decoration: InputDecoration(
                            labelText: 'Password',
                            border: const OutlineInputBorder(),
                            suffixIcon: IconButton(
                                icon: Icon(_showPw ? Icons.visibility_off : Icons.visibility),
                                onPressed: () => setState(() => _showPw = !_showPw),
                            ),
                        ),
                        obscureText: !_showPw,
                        textInputAction: TextInputAction.done,
                        onFieldSubmitted: (_) => _submit(),
                        validator: (v) {
                            if (v == null || v.length < 8) return 'Min 8 characters';
                            if (!RegExp(r'[A-Z]').hasMatch(v)) return 'Needs an uppercase letter';
                            if (!RegExp(r'[0-9]').hasMatch(v)) return 'Needs a digit';
                            return null;
                        },
                        onSaved: (v) => _data['password'] = v!,
                    ),
                    const SizedBox(height: 12),

                    // 4) Checkbox in a form — FormField<bool>
                    FormField<bool>(
                        initialValue: false,
                        validator: (v) => v == true ? null : 'You must accept the terms',
                        builder: (state) => Column(
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: [
                                CheckboxListTile(
                                    title: const Text('I accept the terms'),
                                    value: state.value,
                                    onChanged: (v) => state.didChange(v),
                                    controlAffinity: ListTileControlAffinity.leading,
                                ),
                                if (state.hasError)
                                    Padding(
                                        padding: const EdgeInsets.only(left: 12),
                                        child: Text(state.errorText!,
                                            style: TextStyle(color: Theme.of(context).colorScheme.error, fontSize: 12)),
                                    ),
                            ],
                        ),
                    ),

                    const SizedBox(height: 24),

                    // 5) Submit button
                    SizedBox(
                        width: double.infinity,
                        child: FilledButton(
                            onPressed: _loading ? null : _submit,
                            child: _loading
                                ? const SizedBox(height: 18, width: 18, child: CircularProgressIndicator(strokeWidth: 2))
                                : const Text('Sign up'),
                        ),
                    ),
                ],
            ),
        );
    }
}

// 6) Tips
//   • Use `autovalidateMode: onUserInteraction` for friendly UX — error appears on first blur, not on mount
//   • Wrap inputs in a SingleChildScrollView when the keyboard would obscure them
//   • For complex forms, look at flutter_form_builder + flutter_form_builder_validators
//   • Use TextEditingController if you need to read/clear values imperatively (form save covers most cases)
//   • Submit on `textInputAction: TextInputAction.done` + `onFieldSubmitted`

Why it matters

A GlobalKey<FormState> + validator + onSaved is the entire Flutter forms recipe. FormField<T> wraps non-text widgets (checkboxes, sliders) into the same lifecycle so one validate() call covers everything.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
final _form = GlobalKey<FormState>();
Form(
    key: _form,
    child: TextFormField(
        decoration: const InputDecoration(labelText: 'Email'),
        validator: (v) => v == null || !v.contains('@') ? 'Invalid' : null,
    ),
)
Try it Yourself »

Discussion

Loading…