Relations between fields
addRelation derives one field's value from another — a running total, a mirrored field, a selection cleared when its parent changes.
For "when B changes, set A" — recompute a total, mirror one field into another, clear a dependent selection — use the
form's addRelation. Type a quantity below; the total is a read-only field the relation writes.
Source2 files
class OrderTotalController extends AdvancedFormController {
OrderTotalController() {
registerFields([quantity, total]);
// `select` picks the part of the source to watch; `onChange` fires only
// when that part changes. Nothing to clean up: the form removes the
// listener in its own dispose().
addRelation(
quantity,
(value) => int.tryParse(value) ?? 0,
(count) => total.setValue(count * unitPrice, force: true),
);
}
static const unitPrice = 12.5;
final quantity = AdvancedTextFieldController(
initialValue: '1',
validator: positiveInteger('Enter a whole number above zero'),
);
// A display field: read-only, so only the relation writes it.
final total = AdvancedFieldController<double, String>(
initialValue: unitPrice,
name: 'total',
)..markReadOnly();
}DocsTextField is a shorthand these docs define, not part of the package. See Rendering fields for the widget code an app writes.addRelation(source, select, onChange) calls onChange whenever the part of source's value picked by select
changes, compared with ==. Status-only changes never fire it, so unlike a raw addListener pair a mutual relation
cannot loop. Three details matter in practice:
- It fires on change, not at registration. Seed the target with the derived value as its
initialValue, astotaldoes above, or callonChangeonce by hand right afteraddRelation. - A derived field is usually read-only, and
setValueon a read-only field is a no-op — so the relation passesforce: true. - Destructive relations are safe.
addRelation(country, (c) => c, (_) => city.reset())clears the city when the country changes and nothing else can wipe the user's choice; the wizard does exactly that.
Attaching and detaching subforms, or calling setValidationEnabled, from onChange is supported too — that is how a
switch on one page adds or drops another page of a wizard.
Which one do I want?
| I need to… | Use |
|---|---|
| re-check B's rule when A changes | B.subscribeToFields([A]) |
| re-check every rule when anything changes | validateAll: true |
| re-check every rule because something outside the form changed | form.revalidateSync() |
| set B's value when A changes | addRelation(A, select, (a) => B.setValue(…)) |
| forget B's async verdict when A changes | addRelation(A, select, (_) => B.clearErrors()) |
| react to A outside the form (analytics, a service) | A.addListener(…), comparing values yourself |
Cross-field rules
subscribeToFields re-runs a field's sync validator when the fields it depends on change — repeat-password in two lines.
AdvancedFormState
The form controller is a ValueListenable of AdvancedFormState — canSubmit, wasModified, validating and validationErrors, derived from the live tree.