Cross-field rules
subscribeToFields re-runs a field's sync validator when the fields it depends on change — repeat-password in two lines.
Two things look alike and are not: a rule on field B that reads field A ("passwords must match"), and a value of field B computed from field A ("total is quantity times price"). This page is the first; Relations is the second.
subscribeToFields re-runs this field's sync validator whenever the fields it depends on change value. Mismatch the
two fields below, then fix the first one: the second field's error clears itself.
Source2 files
class PasswordFormController extends AdvancedFormController {
PasswordFormController()
: super(validationMode: ValidationMode.onUserInteraction) {
registerFields([password, repeatPassword]);
}
final password = AdvancedTextFieldController(
validator: atLeastLength(8, 'Password is too short'),
);
// `late`, because the validator reads a sibling.
late final repeatPassword = AdvancedTextFieldController(
validator: (value) =>
value == password.fieldValue ? null : 'Passwords do not match',
)..subscribeToFields([password]);
}DocsTextField is a shorthand these docs define, not part of the package. See Rendering fields for the widget code an app writes.What it does, precisely
- It re-runs this field's sync validator. It does not copy or derive values, and it does not re-run the async validator — this field's own value did not change, so its verdict still stands.
- It obeys the gate: nothing happens while this field is in
ValidationMode.manual, and nothing on a field the user has never edited. Undermanualthe mismatch shows up on submit instead. - It fires when a watched value changes, never on a status-only change of the sibling, so it cannot loop.
- A second call replaces the previous subscription; the subscription is dropped on
dispose. - A code pushed with
setErrorgives way to whatever the validator now returns, like any other re-run.
Two fields watching each other
One rule spanning a pair — "book at least one person" across adults and children — needs the rule in both
validators and a subscription in both directions. Wire the subscriptions in the constructor body, after
registerFields, never in the field initialisers: a ..subscribeToFields([sibling]) cascade inside a late final
initialiser evaluates the sibling eagerly, and a mutual pair fails at construction.
BookingFormController() {
registerFields([adults, children]);
adults.subscribeToFields([children]);
children.subscribeToFields([adults]);
}
late final adults = AdvancedTextFieldController<MyError>(
validator: (_) => _somebodyTravels() ? null : MyError.nobodyTravels,
);
late final children = AdvancedTextFieldController<MyError>(
validator: (_) => _somebodyTravels() ? null : MyError.nobodyTravels,
);
bool _somebodyTravels() =>
(int.tryParse(adults.fieldValue) ?? 0) +
(int.tryParse(children.fieldValue) ?? 0) >
0;An error only ever appears on a field whose own validator returns it, which is why the rule is duplicated; the subscriptions are only what re-runs it.
Everything depends on everything
Pass validateAll: true to the superclass and any field's value change re-runs the sync validator on every
autovalidating field in the tree — blunt but correct when most fields cross-validate. Like subscribeToFields, it
re-runs sync validators only, so typing in one field never starts a network check on another. The same broadcast is
available on demand as form.revalidateSync(), for something other than a field value that changes what "valid"
means — a locale switch, a feature flag.
Async failures and patterns
A validator that throws is a failure, not an error. What the field does with it, and the patterns async checks tend to need.
Relations between fields
addRelation derives one field's value from another — a running total, a mirrored field, a selection cleared when its parent changes.