FAQ and gotchas
The questions that come up in review, the type errors that look like package bugs and are not, and the behaviours that surprise on first contact.
Validation
No. valid means no error recorded, not checked and passed, and a field nobody has validated has no error. Use
canSubmit to grey out a button over known errors; use await validate() to decide a submit. Do not gate a
validate-on-submit button on canSubmit at all — it turns false while a check is in flight and while a field sits in
failedValidation, exactly when the user needs to retry. See Form-level state.
The default mode is manual: nothing validates until the first validate(). That is deliberate — a form should not
shout before the user has done anything. For live feedback give the form ValidationMode.onUserInteraction; for
feedback when a field is left, ValidationMode.onUnfocus — which also needs the widget to bind field.focusNode. And
in every mode a field the user has never edited stays quiet on its own. See Validation modes.
Two gates sit in front of it: the dependent field must be in a mode other than manual, and the user must have edited
it at least once. Under manual the mismatch shows up on submit instead. Also check that the value of a watched
field changed — a status-only change never fires it. See Cross-field logic.
Both errors are cleared when an async round starts, because they described the previous value, so the message is
blank for the debounce plus the request and returns if the answer is still bad. To keep the old text up during the
check, render state.error only while !state.isInProgress.
A field is probably on failedValidation: its async validator threw or timed out. That is not valid, so validate()
is false, but the field carries no error code unless you supplied failureToError. Bind form.value.hasFailedValidation
to a banner, and check field.lastFailure for the exception. See Async validation.
There is no status for it yet — valid covers both. Keep your own flag: set it after await validate() returned true,
clear it on onValuesChanged. A FieldStatus.unverified that would distinguish the two is the planned next
state-model change; see Under the hood.
Not into two validation passes: concurrent validate() calls share one future. Your save request is another
matter — the package knows nothing about it. Keep a flag on the controller and set it before the first await. See
Form-level state.
Types
The built-in string validators are Validator<String?, E>, and & needs both sides to have the same T. An
unannotated closure (value) => … infers String. Annotate it — (String? value) => … — or write custom string rules
over String? like the built-ins. See Validators.
E was inferred as Object, which happens whenever the constructor has no validator to infer it from —
AdvancedTextFieldController(), AdvancedBooleanFieldController(). Spell it out:
AdvancedTextFieldController<MyError>(). The same applies to a map of mixed fields:
Map<String, AdvancedFieldController<dynamic, MyError>>.
mustBeTrue(MyError.mustAccept) covers the checkbox that has to be ticked. There is no Set validator, and notEmpty
is for List, so a rule on a multi-select is a one-line closure: (chosen) => chosen.isEmpty ? MyError.pickOne : null.
That is Flutter, not the package: the selected-value parameter is value: up to Flutter 3.33 and initialValue: from
3.35 on. A DropdownButton inside an InputDecorator compiles on every version and, unlike a form field seeded with
an initial value, always shows the field's current value — including after reset(). See
Rendering fields.
Widgets
No. advanced_forms is the validation system; a second one fights it. Use TextFormField for its decoration if you
like — outside a Form it is a TextField — and never set autovalidateMode.
The widget is bound to a TextEditingController of its own. Bind controller: field.textController instead: the field
owns that controller and keeps it in two-way sync. Delete the onChanged and initialValue plumbing while you are
there.
TextField.readOnly blocks typing but keeps the enabled styling. Pass enabled: !state.readOnly as well. Widgets
with a nullable callback — Switch, Checkbox, Dropdown, Slider — grey out on their own once
field.getValueSetter() returns null.
Yes. A controller is a ChangeNotifier and a ValueListenable; nothing in the package knows about any DI or
state-management library. Own it wherever you own other notifiers, subscribe with AdvancedFieldBuilder or
ValueListenableBuilder, and dispose it once.
Lifecycle
The baseline is captured at registerFields time, and the form was filled after that — by prefill, setValue or a
relation. Build the field controllers with the loaded data as initialValue:, or re-call registerFields once the
data is in. See Lifecycle and ownership.
Something disposed it twice, or used it after its form was disposed. The form disposes every registered field and every attached subform; you dispose only the form. A field or subform that must outlive a screen needs an owner above that screen. See Lifecycle and ownership.
Either the parent or the subform was already disposed. removeSubform does not dispose — the parent still owns the
subform and disposes it with itself — so a section that comes and goes can be re-attached; a section that was disposed
needs a fresh controller. Consider setValidationEnabled(false) instead of detaching.
0.1.x controllers were cubits, so they had a stream. 0.2 is Listenable-based: addListener,
subscribeToFields, onValuesChanged, onStatusChanged, and the builder widgets. A deprecated
AdvancedFieldController.stream extension exists as a migration bridge and is removed in 0.3.0. See
Migration.
Something else?
If the docs do not answer it, the example app probably has a screen for it, and the issue tracker is where questions turn into docs.