skip to content

In Flutter, who should create and dispose a FocusNode, and what breaks when one is created inside build()?

level: middleimportance: must knowfreq 55%

answer

  1. long-lived handle, like a controller
  2. create once, release in dispose()
  3. Focus widget: owned versus hosted node
  4. a new node every rebuild
  5. requestFocus deferred until attached

basics

~20 s

A FocusNode is a long-lived object owned by a State: create it once and call dispose() in State.dispose. Creating one in build() swaps in a fresh node on every rebuild, leaking old nodes and dropping focus mid-typing.

solid answer

~50 s

`FocusNode` keeps focus state alive across rebuilds, so it belongs to a `State`: create it once (a field initializer or `initState`), pass it to a `TextField` or `Focus`, and call `dispose()` in `State.dispose`. A `Focus` widget that receives no node allocates, owns and disposes its own; a node you pass in is only *hosted* — the widget attaches, detaches and reparents it, but disposing it is your job. Allocating the node in `build()` hands the tree a new node on every rebuild: the old ones are never disposed, and the field can lose focus mid-typing. Don't share one node between two widgets, and prefer `node.requestFocus()` over `FocusScope.of(context).requestFocus(node)` — they are equivalent and the first is cheaper. A request made before the node is attached is deferred until it joins the tree, and the change is applied in a microtask, not synchronously.

code

dart · 32 lines
dart
import 'package:flutter/material.dart';

class TenderAmountRow extends StatefulWidget {
  const TenderAmountRow({super.key});

  @override
  State<TenderAmountRow> createState() => _TenderAmountRowState();
}

class _TenderAmountRowState extends State<TenderAmountRow> {
  // Created once, owned by this State.
  final FocusNode _amountFocus = FocusNode(debugLabel: 'tender amount');

  @override
  void dispose() {
    _amountFocus.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Row(
      children: <Widget>[
        Expanded(child: TextField(focusNode: _amountFocus)),
        TextButton(
          onPressed: () => _amountFocus.requestFocus(),
          child: const Text('Edit amount'),
        ),
      ],
    );
  }
}

go deeper

for a junior

Recall the pattern: FocusNode is created once in a State, passed to the TextField, and disposed in dispose(), exactly like a controller.

for a middle

Explain owned versus hosted nodes on the Focus widget, why a node made in build() loses focus on rebuild, and that requestFocus is applied in a microtask.

for a senior

Show how you would diagnose focus that disappears after a rebuild or a leak report naming FocusNode, and how you pass one node down as a handle instead of sharing it.

for a principal

Frame focus nodes as resources with an owner, and argue for a team rule that only the widget which disposes a node may create it.

## What a FocusNode is Flutter directs keyboard input with a **focus tree** that runs parallel to the widget tree. Each node in it is a `FocusNode` (or its subclass `FocusScopeNode`, which groups nodes and remembers which child was focused last). Exactly one node at a time holds the **primary focus**, exposed as `FocusManager.instance.primaryFocus`; key events start there and bubble up through its ancestors. Widgets are rebuilt constantly, but focus has to survive those rebuilds — a text field must not forget it is being edited because its parent called `setState`. That is why a `FocusNode` is a **long-lived object**, closer to a render object or a controller than to a widget. It is also a `ChangeNotifier`, so it holds listeners and must be released with `dispose()`. The Flutter focus guide describes the node's main public use as an **opaque handle**: an ancestor creates it, passes it to a descendant, and later calls `requestFocus()` on it to move focus there. ## Where to create it and where to release it The rules from the focus guide, in practice: 1. **Create the node once**, in a `State` — a `final` field initializer or `initState`. 2. **Pass it** to the widget that should be focusable: `TextField(focusNode: ...)` or `Focus(focusNode: ...)`. 3. **Dispose it** in `State.dispose`, before calling `super.dispose()`. 4. **Give it a `debugLabel`** so focus-tree dumps and debug output name it. 5. **Use one node per widget.** Two widgets sharing a node fight over its attributes and parent. A `StatelessWidget` has nowhere to dispose a node, so it should never create one. ## Owned versus hosted nodes | How the node reaches the widget | Who allocates it | Who disposes it | |---|---|---| | `Focus(child: ...)` with no `focusNode` | the `Focus` widget | the `Focus` widget | | `Focus(focusNode: myNode, ...)` | your `State` | your `State` — the widget only *hosts* it | | `TextField()` with no `focusNode` | the text field's state | the text field's state | | `TextField(focusNode: myNode)` | your `State` | your `State` | The `Focus` API docs spell out the split: a supplied node is *hosted, not owned* — the widget attaches, detaches and reparents it as the tree changes, but the owner stays responsible for `dispose()`. Only supply a node when an ancestor needs to control focus; otherwise let `Focus` manage its own and configure it through the widget's parameters (`autofocus`, `canRequestFocus`, `skipTraversal`, `onKeyEvent`, `onFocusChange`). ## What goes wrong when build() creates the node Writing `TextField(focusNode: FocusNode())` inside `build()` looks harmless and fails in two ways: - **Leaks.** Every rebuild allocates a node that is never disposed, together with any listeners attached to it. - **Lost focus.** When the parent rebuilds while the field is focused, the field is handed a brand-new node that does not have focus, so the caret vanishes and — on mobile — the soft keyboard can close mid-word. - **Broken handles.** Any code that stored the old node and later calls `requestFocus()` on it is talking to a node that is no longer in the tree. The same reasoning applies to a `FocusScopeNode` you create yourself. ## Requesting and observing focus - `node.requestFocus()` asks for primary focus for that node and its ancestors. The guide notes it is equivalent to, and cheaper than, `FocusScope.of(context).requestFocus(node)`. - **Deferred requests.** If the node has not been attached yet — for example you call `requestFocus()` in `initState` before the child that hosts it is built — the request is remembered and applied when the node joins the tree. - **Asynchronous notification.** The focus manager applies the change in a microtask, so `hasPrimaryFocus` may still read `false` on the line after `requestFocus()`; listeners and `Focus.onFocusChange` fire once the change is applied. - `hasPrimaryFocus` is true for the one node that receives key events; `hasFocus` is true when the node **or any descendant** has primary focus. - `canRequestFocus: false` makes a node refuse `requestFocus()`; `autofocus: true` on a `Focus` or `TextField` requests focus when it is first built if nothing else in its scope is focused. ## Common mistakes - Setting `onKeyEvent` directly on a node that a `Focus` widget manages; the widget re-applies its own parameters on the next build and can overwrite it. Wrap the subtree in another `Focus` instead. - Forgetting `dispose()` on a node created in `initState`; `FocusNode` is a `ChangeNotifier`, so leak tracking can report it as an undisposed object. - Calling `requestFocus()` and then reading `hasFocus` synchronously in the same function to decide what to do next.

  • Why can hasPrimaryFocus still be false right after calling requestFocus() in Flutter?
    `requestFocus()` only marks the node for focus; the `FocusManager` applies the change in a microtask, and the API docs warn that notification can lag the request by up to one frame. If the node is not attached yet, the request waits until it is reparented into the tree. React to `Focus.onFocusChange` or a node listener instead of reading the flag on the next line.
  • In Flutter, what is the difference between hasFocus and hasPrimaryFocus on a FocusNode?
    `hasPrimaryFocus` is true only for the single node that is `FocusManager.instance.primaryFocus` — the one key events start from. `hasFocus` is true when the node or any of its descendants holds primary focus, so a `FocusScopeNode` or a `Focus` wrapping a whole form reports `hasFocus` while one of its fields is being edited.
  • Should you assign onKeyEvent directly on a FocusNode that a Focus widget hosts?
    No. The `Focus` widget pushes its own parameters onto the node when it builds, so a callback set straight on the node can be overwritten on the next build. Put the handler on the `Focus` widget, or wrap the subtree in another `Focus` with its own `onKeyEvent` and `canRequestFocus: false` if it should never take focus itself.

saying these in an interview costs you the question

  • FocusNode is lightweight, so creating one in build() is fine
  • A Focus widget disposes the FocusNode you pass to it
  • Sharing one FocusNode between two fields keeps them in sync
  • hasPrimaryFocus is true on the very next line after requestFocus()
  • You must call FocusScope.of(context).requestFocus(node) instead of node.requestFocus()