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.

focus_form.dartidle
Source2 files
focus_form_controller.dart
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 own dispose().
  • A supplied node stays yours. With focusNode: in the constructor, the field only listens.
  • Reading focusNode on a disposed field throws a StateError, 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 debugLabel is AdvancedFieldController(<name>), so giving fields a name makes the focus tree readable in DevTools.

Next

On this page