Focus Management & Field Traversal
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();
}
}
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 specificFocusNodeunfocus()— remove focus from all fields, dismissing the keyboardnextFocus()— advance to the next focusable widget in traversal order (less explicit)hasFocuson a node — check whether a field is currently active
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.
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
FocusNodeper field ininitState; dispose them all indispose. - Set
textInputAction: TextInputAction.nexton every field except the last, which usesTextInputAction.done. - In
onFieldSubmitted, callFocusScope.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 callrequestFocusduring a build frame.