Wizards
One subform per step makes per-step validation one line, and the final submit re-checks every step the user walked over.
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 step, so a step the user walked back over is checked again.
Source3 files
/// One step of the wizard: an ordinary form controller with a title.
abstract class WizardStep extends AdvancedFormController {
String get title;
}
final _emailPattern = RegExp(r'^[^@\s]+@[^@\s]+\.[^@\s]+$');
class AccountStep extends WizardStep {
AccountStep() {
registerFields([email, password]);
}
@override
String get title => 'Account';
final email = AdvancedTextFieldController(
validator: (value) =>
_emailPattern.hasMatch(value) ? null : 'Enter an email',
);
final password = AdvancedTextFieldController(
validator: atLeastLength(8, 'At least 8 characters'),
);
}
class AddressStep extends WizardStep {
AddressStep() {
registerFields([country, city]);
// Picking another country clears the city — a relation, not a rule.
addRelation(country, (value) => value, (_) => city.reset());
}
@override
String get title => 'Address';
final country = AdvancedSingleSelectFieldController<String, String>(
initialValue: null,
options: const ['Poland', 'Germany', 'Spain'],
validator: notNull('Pick a country'),
);
final city = AdvancedTextFieldController(validator: filled('Enter a city'));
}
class ConfirmStep extends WizardStep {
ConfirmStep() {
registerFields([terms]);
}
@override
String get title => 'Confirm';
final terms = AdvancedBooleanFieldController<String>(
validator: mustBeTrue('You have to accept the terms'),
);
}DocsActions, DocsDropdownField, DocsSubmitButton, DocsSwitchField and DocsTextField are shorthands these docs define, not part of the package. See Rendering fields for the widget code an app writes.Three decisions in that code are worth keeping:
- Create the wizard controller above the pages. A controller built inside a step page dies with it, taking that
step's values and errors along. With routes, provide it above the
Navigatoror hand it to each route withChangeNotifierProvider.value. - Escalate the failed step, not the whole form. After a failed
next(), the step getsValidationMode.onUserInteractionso it corrects itself as the user types. Because a subform's own mode wins over the parent's, the steps ahead stay inmanualand never flag an untouched field. - A conditional step comes and goes with its switch. The example app's Step Form screen attaches its invoice step
with
addSubform(invoice, enabled: () => address.needsInvoice.fieldValue). Detached, the step leaves the flow andvalidate(),canSubmitandvalidating, so membership is the whole condition — and the wizard still owns it, so its values survive in case the user flips the switch back, with its errors cleared.
Attaching subforms
addSubform makes another form controller part of this one — its fields join validate, reset and every other broadcast. A dynamic list is the simplest case.
Subform patterns
Detach a section or switch its validation off? Swap one section for another? The trade-offs, and where the example app shows each.