Your first form
A controller holds the fields and registers them; a widget binds to each field.
A controller holds the fields and registers them; a widget binds to each field. The form below is the code on this page, compiled and running — submit it empty to watch the errors appear:
class SignupFormController extends AdvancedFormController {
SignupFormController() {
registerFields([firstName, lastName]);
}
final firstName = AdvancedTextFieldController(
validator: filled('First name is required'),
);
final lastName = AdvancedTextFieldController(
validator: filled('Last name is required'),
);
Future<void> submit() async {
if (await validate()) {
// send it
}
}
}class SignupForm extends StatefulWidget {
const SignupForm({super.key});
@override
State<SignupForm> createState() => _SignupFormState();
}
class _SignupFormState extends State<SignupForm> {
final _form = SignupFormController();
@override
void dispose() {
_form.dispose(); // disposes the registered fields too
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
_SignupTextField(field: _form.firstName, label: 'First name'),
_SignupTextField(field: _form.lastName, label: 'Last name'),
ElevatedButton(onPressed: _form.submit, child: const Text('Submit')),
],
);
}
}class _SignupTextField extends StatelessWidget {
const _SignupTextField({required this.field, required this.label});
final AdvancedTextFieldController<String> field; // <String> is the error type
final String label;
@override
Widget build(BuildContext context) {
return AdvancedFieldBuilder<String, String>(
field: field,
builder: (context, state, _) => TextFormField(
controller: field.textController,
decoration: InputDecoration(labelText: label, errorText: state.error),
),
);
}
}Fields stay quiet until the first validate(), then give live feedback.
Two rules to remember:
- Call
registerFields()once, with every field. The form then owns their lifecycle — it disposes them, tracks whether anything was modified, and includes them invalidate(),resetAll(), and the other form-wide operations. Calling it a second time replaces the field list, so the earlier batch stops participating while still being disposed at teardown. - Bind widgets to
field.textController, not to a controller of your own. See Rendering fields.
Own the controller wherever you like — it's a ChangeNotifier, so any DI package works as well as the StatefulWidget above. ChangeNotifierProvider from provider is one widget, and it disposes the controller for you (provider is used in the snippets below; it is not a dependency of this package). Whoever owns the form controller disposes it, and that one call disposes the registered fields and the attached subforms.