Flutter / Dart, widgets, and local ownership / Lesson 5
Forms, validation, and focus
Distinguish a text draft from a validated command and make failures understandable before submitting.
On this page
Lesson goals
- Treat editing as a distinct state
- Make validation return a reason
- Extract a pure validation rule
Treat editing as a distinct state
A text field contains a draft, not an already valid domain value. Users can temporarily enter an incomplete number or erase a required label while editing. Do not force every keystroke into the saved model or reject intermediate states so aggressively that ordinary correction becomes impossible. Decide when validation runs, how errors are announced, and what event creates the command. A Form groups related fields, while each TextFormField can report its own validation message. Server-side validation and authorization remain necessary even when the client form is carefully designed.
Make validation return a reason
A validator returns null for success or a helpful error string for failure. Its job is to inspect the current value without sending a request or changing unrelated state. Normalize only what the domain permits; trimming a public display label can be reasonable, but silently altering a password would change the userโs secret. Prefer errors that say how to recover, such as entering at least two characters, rather than Invalid. Keep localized messages separate from stable field identifiers used in tests and server error mapping.
Extract a pure validation rule
The first fragment demonstrates a rule for a workshop name. Whitespace normalization is applied consistently before both checks. The rule is intentionally small: it is not a complete international naming policy, and String.length measures Dart string code units rather than user-perceived characters. Requirements based on grapheme clusters need the appropriate character handling and test cases. The examples still establish useful boundary behavior for synthetic ASCII labels. Keeping the rule pure allows its contract to be discussed without a keyboard, rendering tree, or live backend.
String? validateName(String? raw) {
final value = raw?.trim() ?? '';
if (value.isEmpty) return 'Enter a workshop name';
if (value.length < 2) return 'Use at least two characters';
return null;
}
void main() {
print(validateName(' '));
print(validateName('A'));
print(validateName(' Maps '));
}
Enter a workshop name
Use at least two characters
null
The success value null means no validation error. It does not mean the accepted label itself is null.
Keep the form key stable
The second fragment shows the central submission boundary inside an existing State. Its GlobalKey is a field created once, not a fresh key allocated during build. Calling validate asks descendant form fields to evaluate their current drafts and display errors. Only when it returns true should the handler create a normalized command. The fragment records a local preview rather than performing a network write. A real save additionally needs pending state, duplicate-activation policy, server-error handling, and a decision about what happens if the user leaves before completion.
final formKey = GlobalKey<FormState>();
final nameController = TextEditingController();
String? preview;
void previewDraft() {
if (!(formKey.currentState?.validate() ?? false)) return;
setState(() { preview = nameController.text.trim(); });
}
// In build: Form(key: formKey, child: TextFormField(
// controller: nameController, validator: validateName))
// The owning State disposes nameController.
A valid Maps draft becomes preview Maps; invalid input leaves the preview unchanged.
This is a State fragment with surrounding widget scaffolding omitted. A missing FormState is handled as an unsuccessful validation.
Plan keyboard and focus behavior
A keyboard type hints at a suitable input layout but does not guarantee valid input: paste, hardware keyboards, and accessibility tools can supply other characters. Set input actions coherently so users can move to the next field or submit intentionally. Keep visible labels associated with their fields and show persistent context instead of relying only on disappearing hints. When validation fails, preserve the draft and help the user locate the first relevant error. Test the page with the on-screen keyboard visible; its reduced viewport can expose layout assumptions that an empty screen never reveals.
Map remote failures without losing local work
A request can fail after client validation because rules changed, permission is absent, or another user claimed a resource. Keep these failures distinct from local format errors. Preserve user input when retry is meaningful and display a recoverable message without exposing sensitive backend details. A form may need a page-level failure plus a field-specific message; map server fields through stable identifiers rather than matching translated label text. Do not report success merely because an asynchronous handler returned: inspect the result contract and update saved state only after the owning operation confirms success.
Common errors and fixes
Errors disappear and drafts reset during rebuild
- Why it happens
- A fresh form key or controller is created in build.
- How to fix it
- Keep them in the lifecycle owner and dispose owned controllers.
- Prevent it next time
- Rebuild the parent while a validation error is visible.
Numeric keyboard input reaches the model as invalid text
- Why it happens
- The keyboard hint was mistaken for validation.
- How to fix it
- Parse and validate the submitted value explicitly.
- Prevent it next time
- Include pasted and hardware-keyboard input in checks.
A backend failure clears useful work
- Why it happens
- Submission resets controllers before confirmed success.
- How to fix it
- Retain the draft until the operation succeeds or the user cancels.
- Prevent it next time
- Define retry and cancellation behavior before wiring save.
Check your understanding
1. How should a seats field accept a draft of 04 but create a domain value?
Show worked solution
Keep the text during editing. On submission trim permitted whitespace, use an explicit integer parser, reject a missing or out-of-range result, and construct the command with the integer 4. Display formatting and domain validation are separate decisions.
2. Why should a validator not perform a remote reservation check?
Show worked solution
Validation may run repeatedly and expects an immediate error result. A remote availability check needs its own asynchronous state and can become stale before save. Keep local validation pure and let the authoritative save enforce reservation rules.
Key takeaways
- Treat editing as a distinct state
- Map remote failures without losing local work