Entendiendo el sistema de foco de teclado de Flutter
Cómo usar el sistema de foco en tu app de Flutter.
Este artículo explica cómo controlar a dónde se dirige la entrada del teclado. Si estás implementando una aplicación que usa un teclado físico, como la mayoría de las aplicaciones de escritorio y web, esta página es para ti. Si tu app no se usará con un teclado físico, puedes omitir esto.
Resumen
#Flutter viene con un sistema de foco que dirige la entrada del teclado a una parte particular de una aplicación. Para hacer esto, los usuarios "enfocan" la entrada en esa parte de la aplicación al tocar o hacer clic en el elemento de UI deseado. Una vez que eso sucede, el texto ingresado con el teclado fluye hacia esa parte de la aplicación hasta que el foco se mueve a otra parte de la aplicación. El foco también se puede mover presionando un atajo de teclado particular, que normalmente está vinculado a Tab, por lo que a veces se le llama "recorrido de tabulación".
Esta página explora las API utilizadas para realizar estas operaciones en una aplicación
de Flutter, y cómo funciona el sistema de foco. Hemos notado que existe cierta
confusión entre los desarrolladores sobre cómo definir y usar objetos FocusNode.
Si eso describe tu experiencia, salta directamente a las mejores prácticas para
crear objetos FocusNode.
Casos de uso del foco
#Algunos ejemplos de situaciones en las que podrías necesitar saber cómo usar el sistema de foco:
- Recibir/manejar eventos de teclado
- Implementar un componente personalizado que necesita ser enfocable
- Recibir notificaciones cuando cambia el foco
- Cambiar o definir el "orden de tabulación" del recorrido del foco en una aplicación
- Definir grupos de controles que deben recorrerse juntos
- Evitar que algunos controles de una aplicación sean enfocables
Glosario
#A continuación se presentan los términos, tal como los usa Flutter, para los elementos del sistema de foco. Las diversas clases que implementan algunos de estos conceptos se presentan a continuación.
- Árbol de foco (Focus tree) - Un árbol de nodos de foco que normalmente refleja de forma dispersa el árbol de widgets, representando todos los widgets que pueden recibir el foco.
- Nodo de foco (Focus node) - Un solo nodo en un árbol de foco. Este nodo puede recibir el foco, y se dice que "tiene el foco" cuando forma parte de la cadena de foco. Participa en el manejo de eventos de teclado solo cuando tiene el foco.
- Foco primario (Primary focus) - El nodo de foco más lejano de la raíz del árbol de foco que tiene el foco. Este es el nodo de foco donde los eventos de teclado comienzan a propagarse hacia el nodo de foco primario y sus ancestros.
- Cadena de foco (Focus chain) - Una lista ordenada de nodos de foco que comienza en el nodo de foco primario y sigue las ramas del árbol de foco hasta la raíz del árbol de foco.
- Alcance de foco (Focus scope) - Un nodo de foco especial cuyo trabajo es contener un grupo de otros nodos de foco y permitir que solo esos nodos reciban el foco. Contiene información sobre qué nodos fueron enfocados previamente en su subárbol.
- Recorrido de foco (Focus traversal) - El proceso de moverse de un nodo enfocable a otro en un orden predecible. Esto se ve típicamente en las aplicaciones cuando el usuario presiona Tab para moverse al siguiente control o campo enfocable.
FocusNode y FocusScopeNode
#Los objetos FocusNode y FocusScopeNode
implementan la
mecánica del sistema de foco. Son objetos de larga duración (más duraderos que los widgets,
similares a los RenderObject) que mantienen el State y los atributos del foco para que
sean persistentes entre las construcciones del árbol de widgets. Juntos, forman
la estructura de datos del árbol de foco.
Originalmente se pretendía que fueran objetos orientados al desarrollador utilizados para controlar
algunos aspectos del sistema de foco, pero con el tiempo han evolucionado para implementar principalmente
detalles del sistema de foco. Para evitar romper aplicaciones existentes,
todavía contienen interfaces públicas para sus atributos. Pero, en
general, para lo que son más útiles es para actuar como un manejador relativamente
opaco, pasado a un widget descendiente con el fin de llamar a requestFocus()
en un widget ancestro, lo que solicita que un widget descendiente obtenga el foco.
La configuración de los demás atributos se gestiona mejor mediante un widget Focus
o
FocusScope,
a menos que no los estés usando o estés implementando tu propia
versión de ellos.
Mejores prácticas para crear objetos FocusNode
#Algunas cosas que se deben y no se deben hacer con respecto al uso de estos objetos incluyen:
- No asignes un nuevo
FocusNodepara cada construcción. Esto puede causar fugas de memoria y, ocasionalmente, causa una pérdida de foco cuando el widget se reconstruye mientras el nodo tiene el foco. - Crea objetos
FocusNodeyFocusScopeNodeen un stateful widget.FocusNodeyFocusScopeNodedeben liberarse (disposed) cuando hayas terminado de usarlos, por lo que solo deben crearse dentro del objeto State de un stateful widget, donde puedes sobrescribirdisposepara liberarlos. - No uses el mismo
FocusNodepara múltiples widgets. Si lo haces, los widgets competirán por gestionar los atributos del nodo y es probable que no obtengas lo que esperas. - Establece la
debugLabelde un widget de nodo de foco para ayudar a diagnosticar problemas de foco. - No establezcas el callback
onKeyEventen unFocusNodeoFocusScopeNodesi están siendo gestionados por un widgetFocusoFocusScope. Si deseas un manejadoronKeyEvent, agrega un nuevo widgetFocusalrededor del subárbol de widgets que te gustaría escuchar, y establece el atributoonKeyEventdel widget a tu manejador. EstablececanRequestFocus: falseen el widget si tampoco quieres que pueda tomar el foco primario. Esto se debe a que el atributoonKeyEventen el widgetFocusse puede establecer en otra cosa en una construcción posterior, y si eso sucede, sobrescribe el manejadoronKeyEventque estableciste en el nodo. - Llama a
requestFocus()en un nodo para solicitar que reciba el foco primario, especialmente desde un ancestro que ha pasado un nodo del que es propietario a un descendiente donde deseas enfocar. - Usa
focusNode.requestFocus(). No es necesario llamar aFocusScope.of(context).requestFocus(focusNode). El métodofocusNode.requestFocus()es equivalente y más eficiente.
Desenfocar (Unfocusing)
#Existe una API para decirle a un nodo que "renuncie al foco", llamada
FocusNode.unfocus(). Si bien elimina el foco del nodo, es importante
darse cuenta de que en realidad no existe tal cosa como "desenfocar" todos los nodos. Si un
nodo se desenfoca, debe pasar el foco a otra parte, ya que
siempre hay un foco primario. El nodo que recibe el foco cuando un nodo llama a
unfocus() es el FocusScopeNode más cercano o un nodo enfocado previamente
en ese alcance, dependiendo del argumento disposition otorgado a unfocus().
Si deseas más control sobre a dónde va el foco cuando lo quitas de
un nodo, enfoca explícitamente otro nodo en lugar de llamar a unfocus(), o usa el
mecanismo de recorrido de foco para encontrar otro nodo con los métodos focusInDirection,
nextFocus, o previousFocus en FocusNode.
Al llamar a unfocus(), el argumento disposition permite dos modos para
desenfocar: UnfocusDisposition.scope
y
UnfocusDisposition.previouslyFocusedChild. El predeterminado es scope, el cual otorga
el foco al alcance de foco (focus scope) padre más cercano. Esto significa que si el foco se
mueve posteriormente al siguiente nodo con FocusNode.nextFocus, comienza con el
"primer" elemento enfocable del alcance.
La disposición previouslyFocusedChild buscará en el alcance para encontrar el
hijo previamente enfocado y solicitará el foco en él. Si no hay un hijo previamente
enfocado, es equivalente a scope.
Widget Focus
#El widget Focus posee y gestiona un nodo de foco, y es el elemento principal del
sistema de foco. Gestiona la vinculación y desvinculación del nodo de foco que posee
del árbol de foco, gestiona los atributos y callbacks del nodo de foco, y
tiene funciones estáticas para permitir el descubrimiento de nodos de foco vinculados al
árbol de widgets.
En su forma más simple, envolver el widget Focus alrededor de un subárbol de widgets permite
que ese subárbol de widgets obtenga el foco como parte del proceso de recorrido del foco, o
cada vez que se llama a requestFocus en el FocusNode pasado a este. Cuando se combina
con un detector de gestos que llama a requestFocus, puede recibir el foco cuando se
toca o se hace clic en él.
Podrías pasar un objeto FocusNode al widget Focus para gestionarlo, pero si no lo
haces, este crea el suyo propio. La razón principal para crear tu propio
FocusNode es poder llamar a requestFocus()
en el nodo para controlar el foco desde un widget padre. La mayor parte de la demás
funcionalidad de un FocusNode se accede mejor cambiando los atributos del
propio widget Focus.
El widget Focus se utiliza en la mayoría de los controles propios de Flutter para implementar su
funcionalidad de foco.
Aquí hay un ejemplo que muestra cómo usar el widget Focus para hacer enfocable un control
personalizado. Crea un contenedor con texto que reacciona al recibir el
foco.
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
static const String _title = 'Focus Sample';
@override
Widget build(BuildContext context) {
return MaterialApp(
title: _title,
home: Scaffold(
appBar: AppBar(title: const Text(_title)),
body: const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[MyCustomWidget(), MyCustomWidget()],
),
),
);
}
}
class MyCustomWidget extends StatefulWidget {
const MyCustomWidget({super.key});
@override
State<MyCustomWidget> createState() => _MyCustomWidgetState();
}
class _MyCustomWidgetState extends State<MyCustomWidget> {
Color _color = Colors.white;
String _label = 'Unfocused';
@override
Widget build(BuildContext context) {
return Focus(
onFocusChange: (focused) {
setState(() {
_color = focused ? Colors.black26 : Colors.white;
_label = focused ? 'Focused' : 'Unfocused';
});
},
child: Center(
child: Container(
width: 300,
height: 50,
alignment: Alignment.center,
color: _color,
child: Text(_label),
),
),
);
}
}
Eventos de teclado
#Si deseas escuchar eventos de teclado en un subárbol,
establece el atributo onKeyEvent del widget Focus para que
sea un manejador que simplemente escuche la tecla, o
maneje la tecla y detenga su propagación a otros widgets.
Los eventos de teclado comienzan en el nodo de foco con foco primario.
Si ese nodo no devuelve KeyEventResult.handled desde
su manejador onKeyEvent, entonces a su nodo de foco padre se le entrega el evento.
Si el padre no lo maneja, pasa a su padre,
y así sucesivamente, hasta llegar a la raíz del árbol de foco.
Si el evento llega a la raíz del árbol de foco sin ser manejado, entonces
se devuelve a la plataforma para dárselo a
el siguiente control nativo de la aplicación
(en caso de que la UI de Flutter forme parte de una UI de aplicación nativa más grande).
Los eventos que se manejan no se propagan a otros widgets de Flutter,
y tampoco se propagan a widgets nativos.
Aquí hay un ejemplo de un widget Focus que absorbe cada tecla que
su subárbol no maneja, sin poder ser el foco primario:
@override
Widget build(BuildContext context) {
return Focus(
onKeyEvent: (node, event) => KeyEventResult.handled,
canRequestFocus: false,
child: child,
);
}
Los eventos de teclado de foco se procesan antes que los eventos de entrada de texto, por lo que manejar un evento de teclado cuando el widget de foco rodea un campo de texto evita que esa tecla se ingrese en el campo de texto.
Aquí hay un ejemplo de un widget que no permitirá que la letra "a" se escriba en el campo de texto:
@override
Widget build(BuildContext context) {
return Focus(
onKeyEvent: (node, event) {
return (event.logicalKey == LogicalKeyboardKey.keyA)
? KeyEventResult.handled
: KeyEventResult.ignored;
},
child: const TextField(),
);
}
Si la intención es la validación de la entrada, la funcionalidad de este ejemplo probablemente
se implementaría mejor usando un TextInputFormatter, pero la técnica aún puede
ser útil: el widget Shortcuts usa este método para manejar atajos antes de
que se conviertan en entrada de texto, por ejemplo.
Controlar qué obtiene el foco
#Uno de los aspectos principales del foco es controlar qué puede recibir el foco y cómo.
Los atributos canRequestFocus, skipTraversal, y descendantsAreFocusable
controlan cómo este nodo y sus descendientes participan en el proceso de foco.
Si el atributo skipTraversal es true, entonces este nodo de foco no participa
en el recorrido del foco. Sigue siendo enfocable si se llama a requestFocus en su
nodo de foco, pero por lo demás se omite cuando el sistema de recorrido de foco está buscando
el siguiente elemento en el que enfocarse.
El atributo canRequestFocus, como era de esperar, controla si el
nodo de foco que gestiona este widget Focus se puede usar o no para solicitar el foco. Si
este atributo es false, entonces llamar a requestFocus en el nodo no tiene efecto.
También implica que este nodo se omite para el recorrido del foco, ya que no puede
solicitar el foco.
El atributo descendantsAreFocusable controla si los descendientes de este
nodo pueden recibir el foco, pero aún permite que este nodo reciba el foco. Este
atributo se puede usar para desactivar la capacidad de enfoque para todo un subárbol de widgets.
Así es como funciona el widget ExcludeFocus: es solo un widget Focus
con
este atributo establecido.
Autofocus
#Establecer el atributo autofocus de un widget Focus le dice al widget que
solicite el foco la primera vez que el alcance de foco al que pertenece recibe el foco. Si
más de un widget tiene establecido autofocus, entonces es arbitrario cuál
recibe el foco, así que intenta establecerlo solo en un widget por alcance de foco.
El atributo autofocus solo surte efecto si no hay ya un foco en
el alcance al que pertenece el nodo.
Establecer el atributo autofocus en dos nodos que pertenecen a diferentes alcances de
foco está bien definido: cada uno se convierte en el widget enfocado cuando se enfocan sus
correspondientes alcances.
Notificaciones de cambio
#El callback Focus.onFocusChanged se puede usar para recibir notificaciones de que el
estado de foco para un nodo en particular ha cambiado. Notifica si el nodo se agrega o
se elimina de la cadena de foco, lo que significa que recibe notificaciones incluso si
no es el foco primario. Si solo quieres saber si has recibido el
foco primario, verifica si hasPrimaryFocus es true en el nodo de foco.
Obtener el FocusNode
#A veces, es útil obtener el nodo de foco de un widget Focus para
interrogar sus atributos.
Para acceder al nodo de foco desde un ancestro del widget Focus, crea y pasa
un FocusNode como el atributo focusNode del widget Focus.
Debido a que necesita
ser liberado (disposed), el nodo de foco que pases debe ser propiedad de un stateful
widget, así que no crees uno cada vez que se construye.
Si necesitas acceso al nodo de foco desde el descendiente de un widget Focus,
puedes llamar a Focus.of(context) para obtener el nodo de foco del widget
Focus más cercano al contexto dado. Si necesitas obtener el FocusNode de un widget
Focus
dentro de la misma función de construcción, usa un Builder
para asegurarte de tener
el contexto correcto. Esto se muestra en el siguiente ejemplo:
@override
Widget build(BuildContext context) {
return Focus(
child: Builder(
builder: (context) {
final bool hasPrimary = Focus.of(context).hasPrimaryFocus;
print('Building with primary focus: $hasPrimary');
return const SizedBox(width: 100, height: 100);
},
),
);
}
Temporización (Timing)
#Uno de los detalles del sistema de foco es que cuando se solicita el foco, este solo surte efecto después de que se completa la fase de construcción actual. Esto significa que los cambios de foco siempre se retrasan un fotograma, porque cambiar el foco puede hacer que partes arbitrarias del árbol de widgets se reconstruyan, incluidos los ancestros del widget que actualmente solicita el foco. Debido a que los descendientes no pueden ensuciar a sus ancestros, tiene que suceder entre fotogramas, para que cualquier cambio necesario pueda suceder en el siguiente fotograma.
Widget FocusScope
#El widget FocusScope es una versión especial del widget Focus que gestiona
un FocusScopeNode en lugar de un FocusNode. El FocusScopeNode
es un
nodo especial en el árbol de foco que sirve como mecanismo de agrupación para los nodos de foco
en un subárbol. El recorrido del foco permanece dentro de un alcance de foco a menos que un nodo fuera
del alcance sea enfocado explícitamente.
El alcance de foco también realiza un seguimiento del foco actual y el historial de los nodos enfocados dentro de su subárbol. De esa manera, si un nodo libera el foco o se elimina cuando tenía el foco, el foco se puede devolver al nodo que tenía el foco previamente.
Los alcances de foco también sirven como un lugar al que devolver el foco si ninguno de los descendientes tiene el foco. Esto permite que el código de recorrido de foco tenga un contexto inicial para encontrar el siguiente (o primer) control enfocable al que moverse.
Si enfocas un nodo de alcance de foco, primero intenta enfocar el nodo actual o el más recientemente enfocado en su subárbol, o el nodo en su subárbol que solicitó autofocus (si lo hay). Si no existe tal nodo, recibe el foco él mismo.
Widget FocusableActionDetector
#El FocusableActionDetector
es un widget que combina la funcionalidad de
Actions,
Shortcuts, MouseRegion
y un widget Focus para crear
un detector que define acciones y vinculaciones de teclas, y proporciona callbacks para
manejar los resaltados de foco y desplazamientos del mouse. Es lo que usan los controles de Flutter para
implementar todos estos aspectos de los controles. Simplemente está implementado utilizando los
widgets constituyentes, por lo que si no necesitas toda su funcionalidad, puedes usar solo
los que necesites, pero es una forma conveniente de incorporar estos comportamientos en
tus controles personalizados.
Controlar el recorrido del foco
#Una vez que una aplicación tiene la capacidad de enfocarse, lo siguiente que muchas apps quieren hacer es permitir que el usuario controle el foco mediante el teclado u otro dispositivo de entrada. El ejemplo más común de esto es el "recorrido por tabulación", donde el usuario presiona Tab para ir al "siguiente" control. Controlar lo que significa "siguiente" es el tema de esta sección. Este tipo de recorrido lo proporciona Flutter de forma predeterminada.
En un layout de cuadrícula simple, es bastante fácil decidir qué control es el siguiente. Si no estás al final de la fila, entonces es el de la derecha (o izquierda para configuraciones regionales de derecha a izquierda). Si estás al final de una fila, es el primer control de la siguiente fila. Desafortunadamente, las aplicaciones rara vez se disponen en cuadrículas, por lo que a menudo se necesita más orientación.
El algoritmo predeterminado en Flutter (ReadingOrderTraversalPolicy) para el recorrido
del foco es bastante bueno: da la respuesta correcta para la mayoría de las aplicaciones.
Sin embargo, siempre hay casos patológicos o casos en los que el contexto o
el diseño requieren un orden diferente al que llega el algoritmo de ordenamiento predeterminado.
Para esos casos, existen otros mecanismos para lograr el
orden deseado.
Widget FocusTraversalGroup
#El widget FocusTraversalGroup
debe colocarse en el árbol alrededor de los subárboles de widgets
que deben recorrerse por completo antes de pasar a otro widget o
grupo de widgets. El solo hecho de agrupar widgets en grupos relacionados suele ser suficiente para
resolver muchos problemas de ordenamiento del recorrido por tabulación. Si no es así, al grupo también se le puede
asignar una FocusTraversalPolicy
para determinar el ordenamiento dentro del grupo.
La ReadingOrderTraversalPolicy predeterminada
suele ser suficiente, pero en
los casos en que se necesita más control sobre el ordenamiento, se puede usar una
OrderedTraversalPolicy.
El argumento order del
widget FocusTraversalOrder
envuelto alrededor de los componentes enfocables
determina el orden. El orden puede ser cualquier subclase de FocusOrder, pero
se proporcionan NumericFocusOrder
y LexicalFocusOrder.
Si ninguna de las políticas de recorrido de foco proporcionadas es suficiente para tu aplicación, también podrías escribir tu propia política y usarla para determinar cualquier ordenamiento personalizado que desees.
Aquí hay un ejemplo de cómo usar el widget FocusTraversalOrder para recorrer una
fila de botones en el orden DOS, UNO, TRES usando NumericFocusOrder.
class OrderedButtonRow extends StatelessWidget {
const OrderedButtonRow({super.key});
@override
Widget build(BuildContext context) {
return FocusTraversalGroup(
policy: OrderedTraversalPolicy(),
child: Row(
children: <Widget>[
const Spacer(),
FocusTraversalOrder(
order: const NumericFocusOrder(2),
child: TextButton(child: const Text('ONE'), onPressed: () {}),
),
const Spacer(),
FocusTraversalOrder(
order: const NumericFocusOrder(1),
child: TextButton(child: const Text('TWO'), onPressed: () {}),
),
const Spacer(),
FocusTraversalOrder(
order: const NumericFocusOrder(3),
child: TextButton(child: const Text('THREE'), onPressed: () {}),
),
const Spacer(),
],
),
);
}
}
FocusTraversalPolicy
#La FocusTraversalPolicy es el objeto que determina qué widget es el siguiente,
dada una solicitud y el nodo de foco actual. Las solicitudes (funciones miembro) son
cosas como findFirstFocus, findLastFocus, next,
previous, e
inDirection.
FocusTraversalPolicy es la clase base abstracta para políticas concretas, como
ReadingOrderTraversalPolicy, OrderedTraversalPolicy y las
clases DirectionalFocusTraversalPolicyMixin.
Para usar una FocusTraversalPolicy, le das una a un
FocusTraversalGroup, el cual determina el subárbol de widgets en el que la política
será efectiva. Rara vez se llama directamente a las funciones miembro de la clase:
están destinadas a ser utilizadas por el sistema de foco.
El administrador de foco (focus manager)
#El FocusManager
mantiene el foco primario actual del sistema.
Solo tiene unas pocas partes de API que son útiles para los usuarios del sistema de foco. Una
es la propiedad FocusManager.instance.primaryFocus, que contiene el
nodo de foco actualmente enfocado y también es accesible desde el campo global
primaryFocus.
Otras propiedades útiles son FocusManager.instance.highlightMode y
FocusManager.instance.highlightStrategy. Estas son utilizadas por widgets que necesitan
cambiar entre un modo "táctil" y un modo "tradicional" (mouse y teclado)
para sus resaltados de foco. Cuando un usuario usa el toque para navegar, el resaltado de
foco suele estar oculto, y cuando cambia a un mouse o teclado, el
resaltado de foco debe mostrarse nuevamente para que sepa qué está enfocado. La
highlightStrategy le dice al administrador de foco cómo interpretar los cambios en el
modo de uso del dispositivo: puede cambiar automáticamente entre los dos
en función de los eventos de entrada más recientes, o puede bloquearse en modos táctil o
tradicional. Los widgets proporcionados en Flutter ya saben cómo usar esta
información, por lo que solo la necesitas si estás escribiendo tus propios controles desde
cero. Puedes usar el callback addHighlightModeListener para escuchar los cambios
en el modo de resaltado.
A menos que se indique lo contrario, la documentación de este sitio refleja Flutter 3.44.0. Página actualizada por última vez el 2026-05-11. Ver código fuente oreportar un problema.