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…