Form-level state

AdvancedFormState

The form controller is a ValueListenable of AdvancedFormState — canSubmit, wasModified, validating and validationErrors, derived from the live tree.

AdvancedFormController is itself a ValueListenable<AdvancedFormState>, so form-wide state renders the same way a field does:

ValueListenableBuilder<AdvancedFormState>(
  valueListenable: form,
  builder: (context, state, _) => FilledButton(
    onPressed: state.wasModified && !state.validating ? form.submit : null,
    child: const Text('Save'),
  ),
);

What the state holds

Four members are stored; four are derived from the fields on every read, so they can never report a field or subform that has since been removed.

MemberMeaning
wasModifiedany field's value differs from its value at registerFields time, or an attached subform was modified
fields / subforms / allFieldsthis form's own fields, its attached subforms, and both flattened — own fields in registration order, then each subform's, recursively
validationEnabledfalse once setValidationEnabled(false) was called on this form or an ancestor
validationModethe mode configured for this tree. Not reduced by validationEnabled
validating (derived)an async check is pending or in flight somewhere in the tree
canSubmit (derived)every field is valid right now — see the warning below
hasFailedValidation (derived)some field's async check could not run: it threw or timed out. Drives one form-level banner
validationErrors (derived)every current error in the tree, sync or async, keyed by field controller

Fields inside a subtree with validation switched off are excluded from the derived members, matching what validate() checks, so the two can never disagree.

canSubmit is a snapshot, not a guarantee

valid means no error recorded, not checked and passed. So canSubmit is true on a quiet form nobody has validated yet, false while any check is in flight, and false while a field sits in failedValidation. It is right for greying out a button over known errors and wrong for deciding a submit — await validate() is the guarantee. See The submit button.

Watch it change

Type, clear a field, submit, press Discard. A body containing offline makes the async check fail.

draft_form.dartidle
Source2 files
draft_form_controller.dart
enum DraftError { required, tooLong, slowNetwork }

class DraftFormController extends AdvancedFormController {
  DraftFormController() : super(validationMode: ValidationMode.onUnfocus) {
    registerFields([title, body]);
  }

  // `name` labels the field in the error summary.
  final title = AdvancedTextFieldController(
    name: 'title',
    validator:
        filled(DraftError.required) & notLongerThan(40, DraftError.tooLong),
  );

  late final body = AdvancedTextFieldController(
    name: 'body',
    validator: filled(DraftError.required),
    asyncValidation: AsyncValidation(
      validator: _moderate,
      failureToError: (error, stackTrace) => DraftError.slowNetwork,
    ),
  );

  Future<DraftError?> _moderate(String value) async {
    await Future<void>.delayed(const Duration(milliseconds: 900));
    if (value.contains('offline')) {
      throw Exception('moderation service unreachable');
    }
    return null;
  }

  Future<bool> save() => validate();
}
DocsActions, DocsFailureBanner, DocsFormStatus, DocsHint, DocsSubmitButton and DocsTextField are shorthands these docs define, not part of the package. See Rendering fields for the widget code an app writes.

Listening outside widgets

To react without a builder, form.onValuesChanged and form.onStatusChanged are Listenables covering the whole tree, subforms included: the first fires when any leaf value changes or fields are registered, the second when any leaf's status or error changes. Pass a named callback so you can removeListener it later. With provider, context.select<SignupFormController, bool>((c) => c.value.validating) subscribes one widget to one slice.

On this page