Focus
Every field owns a FocusNode. Use it to jump to the first invalid field, to chain fields on submit, and to make onUnfocus validation work.
Every AdvancedFieldController — not only the text one — has a focusNode, and a focus() shortcut that requests
focus on it. The field creates the node on first use and disposes it with itself. Pass focusNode: to the constructor
to bind a node you already own instead; the field then listens to it but never disposes it.
TextFormField(
controller: field.textController,
focusNode: field.focusNode,
)Binding the node does two things: the field learns when it loses focus, which is what
ValidationMode.onUnfocus reacts to, and your code can move focus by talking to the field
rather than to the widget tree.
Jump to the first invalid field
After a failed submit, focus the first field with an error. allFields is in registration order, own fields first and
then each subform's, so "first" means what the user sees. Press Submit below with the fields empty, then press
Enter in a field to move to the next one.
Source2 files
class FocusFormController extends AdvancedFormController {
FocusFormController() {
registerFields([street, city, postalCode]);
}
final street = AdvancedTextFieldController(validator: filled('Required'));
final city = AdvancedTextFieldController(validator: filled('Required'));
final postalCode = AdvancedTextFieldController(
validator: filled('Required') & atLeastLength(5, 'At least 5 characters'),
);
/// Validates, and on failure focuses the first field that has an error.
Future<bool> submit() async {
if (await validate()) {
return true;
}
for (final field in value.allFields) {
if (field.value.isInvalid) {
field.focus();
break;
}
}
return false;
}
}DocsActions, DocsSubmitButton and DocsTextField are shorthands these docs define, not part of the package. See Rendering fields for the widget code an app writes.In a scrolling form, focusing a text field also scrolls it into view — Flutter's EditableText calls
Scrollable.ensureVisible when it gains focus. The example app's Scroll Form screen shows it with fields spaced far
apart, and reports the failed submit from the controller to the widget over a plain broadcast Stream, the one-off UI
event pattern.
For fields with no text input — a dropdown, a chip group — focus() still moves keyboard focus to whatever widget is
bound to field.focusNode, but there is nothing for the keyboard to type into. Scroll to or highlight those yourself.
Widgets that manage focus themselves
A TextFormField bound to field.focusNode reports focus loss for free. A picker opened from a tap does not have a
text field to bind, so it tells the field when the interaction is over:
onTap: () async {
final date = await showDatePicker(context: context, /* … */);
if (date != null) {
field.setValue(date);
}
await field.handleUnfocus(); // what onUnfocus reacts to
},handleUnfocus() also flushes a debounced async check in any mode — leaving a half-typed field runs the check now
rather than after the debounce — and it reports its own failures, so nothing escapes into the caller's zone. Awaiting
it is optional.
Rules of the node
- The field owns the node it created. Never dispose
field.focusNode; the field does it in its owndispose(). - A supplied node stays yours. With
focusNode:in the constructor, the field only listens. - Reading
focusNodeon a disposed field throws aStateError, so never touch it from a widget that can outlive its form.focus()on a disposed field is a safe no-op. - The node's
debugLabelisAdvancedFieldController(<name>), so giving fields anamemakes the focus tree readable in DevTools.