Forms, Validation & User Input

Focus Management & Field Traversal

15 min Lesson 6 of 12

Focus Management & Field Traversal

In professional Flutter forms, controlling which field is active at any given moment is just as important as capturing the data itself. Focus management lets you programmatically move the keyboard cursor from one TextFormField to the next, dismiss the keyboard on demand, and respond to focus-change events — all without any gesture from the user beyond the keyboard's own Next or Done action button.

Flutter exposes this system through two complementary classes: FocusNode, which represents the focusable unit attached to a single input widget, and FocusScope, the tree-scoped manager that routes focus requests. Together they give you fine-grained control over the entire keyboard flow in your form.

Understanding FocusNode

A FocusNode is a long-lived object that must be created in initState and disposed in dispose. It carries metadata about whether its widget currently holds focus, and it lets you attach listeners that fire whenever focus is gained or lost.

Creating and Disposing FocusNodes

class RegistrationFormState extends State<RegistrationForm> {
  // One FocusNode per field
  final FocusNode _emailFocus    = FocusNode();
  final FocusNode _passwordFocus = FocusNode();
  final FocusNode _phoneFocus    = FocusNode();

  @override
  void initState() {
    super.initState();
    // Optional: react to focus changes on a specific field
    _emailFocus.addListener(() {
      if (!_emailFocus.hasFocus) {
        // Field lost focus — good time to validate it
        _formKey.currentState?.validate();
      }
    });
  }

  @override
  void dispose() {
    // Always dispose to avoid memory leaks
    _emailFocus.dispose();
    _passwordFocus.dispose();
    _phoneFocus.dispose();
    super.dispose();
  }
}
Note: Never create a FocusNode inside the build method. Each rebuild would create a new instance, causing memory leaks and unpredictable focus behaviour. Always allocate in initState and free in dispose.

Attaching FocusNodes to TextFormFields

Pass the node to the focusNode parameter of TextFormField. Use textInputAction to set the keyboard action button (TextInputAction.next for intermediate fields, TextInputAction.done for the last one). Wire the onFieldSubmitted callback to move focus forward.

Chaining Three Fields with Keyboard Next

final _formKey = GlobalKey<FormState>();
final _emailFocus    = FocusNode();
final _passwordFocus = FocusNode();
final _phoneFocus    = FocusNode();

// Helper: move focus to the next node
void _fieldNext(BuildContext ctx, FocusNode next) {
  FocusScope.of(ctx).requestFocus(next);
}

// Helper: close the keyboard entirely
void _fieldDone(BuildContext ctx) {
  FocusScope.of(ctx).unfocus();
}

@override
Widget build(BuildContext context) {
  return Form(
    key: _formKey,
    child: Column(
      children: [
        TextFormField(
          focusNode: _emailFocus,
          decoration: const InputDecoration(labelText: 'Email'),
          keyboardType: TextInputType.emailAddress,
          textInputAction: TextInputAction.next,          // shows Next
          onFieldSubmitted: (_) =>
              _fieldNext(context, _passwordFocus),        // move forward
        ),
        TextFormField(
          focusNode: _passwordFocus,
          decoration: const InputDecoration(labelText: 'Password'),
          obscureText: true,
          textInputAction: TextInputAction.next,
          onFieldSubmitted: (_) =>
              _fieldNext(context, _phoneFocus),
        ),
        TextFormField(
          focusNode: _phoneFocus,
          decoration: const InputDecoration(labelText: 'Phone'),
          keyboardType: TextInputType.phone,
          textInputAction: TextInputAction.done,           // shows Done
          onFieldSubmitted: (_) => _fieldDone(context),   // dismiss keyboard
        ),
      ],
    ),
  );
}

FocusScope.of(context).requestFocus()

FocusScope.of(context) traverses the widget tree upward to find the nearest FocusScopeNode that owns the given context. Calling .requestFocus(node) on it tells Flutter's focus system to transfer primary focus to that node, which in turn tells the platform to move the software keyboard caret to the corresponding field.

  • requestFocus(node) — move focus to a specific FocusNode
  • unfocus() — remove focus from all fields, dismissing the keyboard
  • nextFocus() — advance to the next focusable widget in traversal order (less explicit)
  • hasFocus on a node — check whether a field is currently active
Tip: Prefer requestFocus(specificNode) over nextFocus() in production forms. nextFocus() follows widget-tree order, which can jump to unexpected widgets (buttons, checkboxes) if your layout is complex. Explicit node references are always predictable.

Programmatic Focus from Buttons or Validation

You are not limited to keyboard action callbacks. Any user interaction — tapping a button, finishing an async call, or detecting a validation error — can shift focus programmatically:

Jump to the First Invalid Field After Submit

void _submit() {
  // Dismiss keyboard first
  FocusScope.of(context).unfocus();

  if (!(_formKey.currentState?.validate() ?? false)) {
    // Re-focus the email field so the user sees the first error
    FocusScope.of(context).requestFocus(_emailFocus);
    return;
  }

  _formKey.currentState!.save();
  // proceed with form data ...
}

AutofillHints and the Focus Chain

Combining autofillHints with a well-ordered focus chain dramatically improves the user experience. When autofill fills a field the keyboard's Next tap still follows your explicit FocusNode chain, so the two features compose cleanly. Always set autofillHints on email, password, and phone fields in registration or login flows.

Warning: Calling requestFocus during a build call will throw a setState called during build error. Always schedule focus changes inside callbacks (onFieldSubmitted, onPressed, addPostFrameCallback) — never inline in build.

Summary

Focus management turns a basic collection of text inputs into a polished, keyboard-friendly form. The key points to remember are:

  • Allocate one FocusNode per field in initState; dispose them all in dispose.
  • Set textInputAction: TextInputAction.next on every field except the last, which uses TextInputAction.done.
  • In onFieldSubmitted, call FocusScope.of(context).requestFocus(nextNode) to chain the fields.
  • Use FocusScope.of(context).unfocus() to dismiss the keyboard when the form is submitted.
  • Never create nodes in build, and never call requestFocus during a build frame.