Field state and statuses
Every read goes through one immutable snapshot, and valid means "no error recorded" — not "checked and passed".
Every read of a field goes through AdvancedFieldState<T, E>, the value of the ValueListenable. It is value-equal,
so setting a field to the value it already holds notifies nobody.
| Member | Meaning |
|---|---|
value | the current value; field.fieldValue is the shortcut |
validationError | what the sync path recorded — the sync validator, or setError |
asyncError | what the async round recorded; only the async pipeline writes it |
error | validationError ?? asyncError — the one to show; field.error is the shortcut |
status | where validation stands, one of the five below |
readOnly | setValue is a no-op until unmarkReadOnly(), or force: true |
validationMode | the effective mode: manual while the form has validation switched off |
Why two error slots? Because a rule about the value (sync) and a verdict from the server (async) can both be true at
once, and the sync rule may be re-run by a sibling's edit while the server's answer still describes this field's
unchanged value. error picks validationError first. Rounds and error slots has the
sequence that made this necessary.
The five statuses
enum FieldStatus { valid, invalid, pending, validating, failedValidation }| Status | Meaning | Shortcut |
|---|---|---|
valid | no error is recorded. Not the same as checked and passed — a field nobody has validated yet is valid | isValid |
invalid | an error is recorded, sync or async | isInvalid |
pending | the value changed and the async validator is waiting out its debounce | isPending |
validating | the async validator is running | isValidating |
failedValidation | the async validator threw or timed out: a technical fault, not a verdict on the value | isFailedValidation |
isInProgress is pending || validating, the one check a spinner needs.
valid means "no error recorded"
This is the most common misunderstanding about the package. A quiet form nobody has checked is valid, so
canSubmit is true on it. Use canSubmit to enable a button; use await validate() to decide a submit. And
!isValid is true for pending and validating too — write isInvalid when you mean "has an error".
Watch it change
Type admin and watch status walk pending → validating → invalid while error is blank in between. Push an error
with setError and then type — the edit clears it. Toggle readOnly and try to type.
Source2 files
class NicknameController extends AdvancedFormController {
NicknameController()
: super(validationMode: ValidationMode.onUserInteraction) {
registerFields([nickname]);
}
late final nickname = AdvancedTextFieldController(
validator: filled('Required') & notLongerThan(12, 'At most 12 characters'),
asyncValidation: AsyncValidation(
validator: _isFree,
debounce: const Duration(milliseconds: 500),
),
);
Future<String?> _isFree(String value) async {
await Future<void>.delayed(const Duration(milliseconds: 700));
return value.toLowerCase() == 'admin' ? 'Reserved on the server' : null;
}
}DocsActions and DocsTextField are shorthands these docs define, not part of the package. See Rendering fields for the widget code an app writes.