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 (comeTextField) ne possiede uno internamente, ma possiamo fornirne uno nostro per controllarlo.FocusScope: raggruppa piùFocusNodein 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 unFocusNode.
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
FocusNodeininitStatee liberali sempre indispose. - Usa
nextFocus()invece di riferimenti manuali quando l'ordine coincide con quello di lettura. - Imposta correttamente
textInputActionper 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 inaddPostFrameCallback.
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.