Perché il focus è importante

La gestione del focus è uno degli aspetti più trascurati nello sviluppo di app Flutter, eppure è fondamentale per l'usabilità. Un buon controllo del focus permette agli utenti di navigare tra i campi di un form con il tasto Tab, di spostarsi automaticamente al campo successivo dopo aver compilato quello corrente e di rendere l'app pienamente utilizzabile con tastiere fisiche (essenziale su Web, desktop e tablet con tastiera).

In questo articolo vedremo come funziona il sistema di focus di Flutter e come sfruttarlo per creare esperienze utente più fluide e accessibili.

Il sistema di focus di Flutter

Flutter organizza il focus in un albero (FocusTree) parallelo al widget tree. I concetti chiave sono:

  • FocusNode: rappresenta un punto che può ricevere il focus. Ogni widget interattivo (come TextField) ne possiede uno internamente, ma possiamo fornirne uno nostro per controllarlo.
  • FocusScope: raggruppa più FocusNode in un contesto di navigazione. Determina l'ordine di traversal e permette di richiedere il focus a livello di gruppo.
  • Focus: widget di basso livello che avvolge un sottoalbero e vi associa un FocusNode.

Spostare il focus tra campi di un form

Lo scenario più comune è passare da un campo all'altro premendo "Invio" o il pulsante "Avanti" della tastiera. Ecco come farlo gestendo manualmente i FocusNode:

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

  @override
  State<LoginForm> createState() => _LoginFormState();
}

class _LoginFormState extends State<LoginForm> {
  final _emailFocus = FocusNode();
  final _passwordFocus = FocusNode();

  @override
  void dispose() {
    _emailFocus.dispose();
    _passwordFocus.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(
          focusNode: _emailFocus,
          textInputAction: TextInputAction.next,
          decoration: const InputDecoration(labelText: 'Email'),
          onSubmitted: (_) {
            // Sposta il focus al campo password
            FocusScope.of(context).requestFocus(_passwordFocus);
          },
        ),
        TextField(
          focusNode: _passwordFocus,
          textInputAction: TextInputAction.done,
          obscureText: true,
          decoration: const InputDecoration(labelText: 'Password'),
          onSubmitted: (_) => _passwordFocus.unfocus(),
        ),
      ],
    );
  }
}

Ricorda sempre di chiamare dispose() sui FocusNode che crei, altrimenti causerai memory leak.

Il metodo nextFocus per il traversal automatico

Se non vuoi gestire manualmente ogni riferimento, puoi lasciare che Flutter calcoli il campo successivo in base all'ordine di traversal:

TextField(
  textInputAction: TextInputAction.next,
  onSubmitted: (_) => FocusScope.of(context).nextFocus(),
)

nextFocus() sposta il focus al nodo successivo secondo la FocusTraversalPolicy attiva, mentre previousFocus() fa l'opposto. Questo approccio è più scalabile perché non richiede un FocusNode per ogni campo.

Personalizzare l'ordine di traversal

Di default Flutter usa la ReadingOrderTraversalPolicy, che segue l'ordine di lettura (dall'alto verso il basso, da sinistra a destra nelle lingue LTR). Puoi però imporre un ordine esplicito con FocusTraversalGroup e FocusTraversalOrder:

FocusTraversalGroup(
  policy: OrderedTraversalPolicy(),
  child: Column(
    children: [
      FocusTraversalOrder(
        order: const NumericFocusOrder(2),
        child: TextField(decoration: InputDecoration(labelText: 'Secondo')),
      ),
      FocusTraversalOrder(
        order: const NumericFocusOrder(1),
        child: TextField(decoration: InputDecoration(labelText: 'Primo')),
      ),
    ],
  ),
)

In questo esempio, nonostante l'ordine visivo, il focus partirà dal campo "Primo".

Reagire ai cambiamenti di focus

Un FocusNode è anche un ChangeNotifier, quindi puoi ascoltarne i cambiamenti per aggiornare l'interfaccia, ad esempio evidenziando un campo attivo:

@override
void initState() {
  super.initState();
  _emailFocus.addListener(() {
    setState(() {}); // ridisegna in base a _emailFocus.hasFocus
  });
}

In alternativa, il widget Focus espone la callback onFocusChange:

Focus(
  onFocusChange: (hasFocus) {
    debugPrint('Focus attivo: $hasFocus');
  },
  child: MyCustomWidget(),
)

Chiudere la tastiera e togliere il focus

Un pattern molto diffuso è chiudere la tastiera quando l'utente tocca al di fuori dei campi. Basta rimuovere il focus dal nodo primario:

GestureDetector(
  onTap: () => FocusScope.of(context).unfocus(),
  child: Scaffold(
    body: /* ... */,
  ),
)

unfocus() rimuove il focus dal nodo corrente, facendo scomparire la tastiera on-screen.

Gestire scorciatoie e focus insieme

Su desktop e Web puoi combinare il focus con le Actions e gli Shortcuts per creare esperienze da tastiera complete. Il widget FocusableActionDetector unisce gestione del focus, hover e azioni in un unico componente, particolarmente utile per pulsanti personalizzati:

FocusableActionDetector(
  onShowFocusHighlight: (focused) => setState(() => _focused = focused),
  onShowHoverHighlight: (hovered) => setState(() => _hovered = hovered),
  child: Container(
    color: _focused ? Colors.blue : Colors.grey,
    child: const Text('Elemento interattivo'),
  ),
)

Best practice

  • Crea i FocusNode in initState e liberali sempre in dispose.
  • Usa nextFocus() invece di riferimenti manuali quando l'ordine coincide con quello di lettura.
  • Imposta correttamente textInputAction per mostrare il pulsante giusto sulla tastiera mobile.
  • Testa la navigazione con Tab su Web e desktop: è un requisito di accessibilità.
  • Evita di richiedere il focus durante il build: fallo in risposta a eventi o in addPostFrameCallback.

Conclusione

Una gestione consapevole del focus migliora sensibilmente l'usabilità delle app Flutter, soprattutto su piattaforme con tastiera fisica. Padroneggiare FocusNode, FocusScope e le policy di traversal ti permette di costruire form fluidi e interfacce pienamente accessibili con poche righe di codice.