Validation

Configuring the mode

Set the mode on the form, let a field or a subform opt out, write values that do not count as edits, and switch a subtree off.

On the form

Pass the mode to the constructor, or change it later with setValidationMode. Both reach the whole tree, and neither validates anything by itself — they only change what the next event does.

class CheckoutFormController extends AdvancedFormController {
  CheckoutFormController()
    : super(validationMode: ValidationMode.onUnfocus) {
    registerFields([email, address]);
  }
}

// Later, for the whole tree:
form.setValidationMode(ValidationMode.onUserInteraction);

One field, or one subform, with its own mode

A field can opt out of its form's mode. Once it has its own, it stops following the form — for good:

final username = AdvancedTextFieldController(
  validator: filled(MyError.required) & atLeastLength(5, MyError.tooShort),
)..setValidationMode(ValidationMode.onUnfocus);

A subform behaves the same way: a validationMode given to its constructor, or set through its own setValidationMode, is its own, and a later change of the parent's mode reaches every other child. This is what makes a wizard step "live" after the user has tried to leave it while the steps ahead stay quiet — see Wizards.

onUnfocus needs a focus node

The field only learns that focus left it through its focusNode. Bind it to the widget, or — for a picker that manages focus itself — call field.handleUnfocus() when the interaction ends:

TextFormField(
  controller: field.textController,
  focusNode: field.focusNode, // the field made this node and owns it
)

// A date picker has no text field to bind, so it says so itself:
final picked = await showDatePicker(/* … */);
field.setValue(picked);
await field.handleUnfocus();

Every field controller has a focusNode, not only text fields. Focus has more.

Values the user did not type

Rule 2 hinges on editing. setValue counts as the user editing the field; prefill does not:

// Loading a profile — no validation, the field stays untouched.
final profile = await api.profile();
name.prefill(profile.name);
email.prefill(profile.email);

// The user typing — validates in onUserInteraction, arms the field for onUnfocus.
name.setValue('Ada');

Both write the value and clear both errors. Only setValue sets the flag that lets the mode fire. So a form filled from the server shows no errors until the user touches a field or presses submit — and validate() on submit still catches a bad prefilled value, because it ignores the flag.

Switching validation off

setValidationEnabled(false) takes a whole subtree out of validation, above every mode: its fields validate nothing, their errors are cleared, its validate() returns true without running, and they are excluded from canSubmit, validating, hasFailedValidation and validationErrors. It composes with a parent's switch by AND. Switching back on re-runs the sync validators at once.

Use it for a section that is visible but not applicable — the invoice details behind an unchecked "I need an invoice" switch — rather than detaching the subform, so resetAll, markReadOnly and dispose still reach it. See Subform patterns.

On this page