Validation
Validation modes
Three rules and one table decide when a field validates itself — on submit, on every keystroke, or when it loses focus.
A validator is a function: value in, error out, null means valid. When it runs is decided by the form's
ValidationMode, and the whole trigger behaviour fits in three rules.
- The mode decides which events make a field validate itself. Set it once on the form; it reaches every field and subform, including ones registered or attached later.
- A field the user has never edited validates nothing on its own, in every mode.
validate()is what checks those, so a prefilled form does not greet the user with errors. - A round runs the sync validator first, and the async validator only if sync passed.
validate() obeys none of them: it ignores the mode, never changes it, and runs every field — touched or not.
The three modes
ValidationMode | What the user sees |
|---|---|
manual (default) | Nothing validates until validate() is called — usually on submit. Editing a field still clears the error that described its old value |
onUserInteraction | Every edit validates the field being edited; the async check waits out its debounce |
onUnfocus | Leaving a field the user edited validates it. Tabbing through an untouched field costs nothing; a debounced async check is flushed on the way out |
The gate, in full — the row is the mode, the column is the event:
| mode | value changed | focus left | a dependency changed |
|---|---|---|---|
manual | — | — | — |
onUserInteraction | validate | — (the edit already did) | sync only |
onUnfocus | — (still typing) | validate | sync only |
"A dependency changed" is what subscribeToFields and validateAll produce; it re-runs the sync
validator only, because this field's own value did not change and its async verdict still stands.
Try it
Switch the mode while you type. The form is the same; only the trigger changes.
Source2 files
class ProfileFormController extends AdvancedFormController {
ProfileFormController() {
registerFields([username, website]);
}
final username = AdvancedTextFieldController(
validator:
filled('Username is required') &
atLeastLength(3, 'At least 3 characters'),
);
final website = AdvancedTextFieldController(
validator: (value) => value.isEmpty || value.startsWith('https://')
? null
: 'Must start with https://',
);
}DocsActions, DocsHint, DocsSubmitButton and DocsTextField are shorthands these docs define, not part of the package. See Rendering fields for the widget code an app writes.