Subforms
Attach nested form controllers so their fields join the parent's validate, reset, and error handling — including wizards, one subform per step.
addSubform attaches another AdvancedFormController as a child, and its fields join the parent's validate, markReadOnly, setValidationEnabled, and the other broadcast operations. Use it for sections that appear dynamically, or to split a large form into readable pieces:
class BaseFormController extends AdvancedFormController {
BaseFormController() {
registerFields([field]);
}
final field = AdvancedTextFieldController();
final subform = SubformController();
void extendForm() => addSubform(subform);
}void removeSubform(form) detaches a subform: it stops validating, notifying and counting towards the parent's state, and can be re-attached later with addSubform. It does not dispose it — the parent owns every subform it was ever given and disposes them all in its own dispose(), the same way it owns registered fields. So don't dispose subforms yourself; if you do, the parent skips them rather than disposing them twice. Calling addSubform with — or on — a disposed controller throws a descriptive StateError rather than crashing later, and so do registerFields, setValidationEnabled and removeSubform on one.
Working examples: DeliveryListFormScreen (a dynamic list), ComplexFormScreen (swapping one subform for another) and StepFormScreen (a wizard: one subform per step).
A wizard, validated step by step
One subform per step is what makes per-step validation one line — await step.validate() reaches that step's fields and nothing else, so a field on a later step is never flagged before the user gets there. The final submit calls the parent's validate(), which walks every attached subform, so a step the user walked back over is checked again:
Future<bool> next() async {
final step = currentStep;
if (!await step.validate()) {
// The step has shown its errors once — let it correct itself as the user
// types. A subform's own mode wins over the parent's, so the steps ahead
// stay quiet.
step.setValidationMode(ValidationMode.onUserInteraction);
return false;
}
_goTo(currentIndex + 1);
return true;
}Which step is on screen is navigation, not form state: keep it on your own controller, and create that controller above the pages — a wizard controller built inside a step page dies with it, taking that step's values and errors along. Working example: StepFormScreen (example/lib/screens/step_form.dart).