Usando Actions y Shortcuts
Cómo usar Actions y Shortcuts en tu app de Flutter.
Esta página describe cómo vincular eventos físicos del teclado a acciones en la interfaz de usuario. Por ejemplo, si quieres definir atajos de teclado en tu aplicación, esta página es para ti.
Resumen
#Para que una aplicación GUI haga algo, tiene que tener acciones: los usuarios quieren decirle a la aplicación que haga algo. Las acciones suelen ser funciones simples que realizan directamente la acción (como establecer un valor o guardar un archivo). En una aplicación más grande, sin embargo, las cosas son más complejas: el código para invocar la acción, y el código para la acción misma podrían necesitar estar en diferentes lugares. Los accesos directos (vinculaciones de teclas) pueden necesitar definirse a un nivel que no sepa nada sobre las acciones que invocan.
Ahí es donde entra el sistema de acciones y atajos de Flutter. Permite
a los desarrolladores definir acciones que cumplan con los intents vinculados a ellas. En este
contexto, un intent es una acción genérica que el usuario desea realizar, y una
instancia de la clase Intent
representa estos intents del usuario en Flutter. Un
Intent puede ser de propósito general, cumplido por diferentes acciones en diferentes
contextos. Un Action
puede ser un simple callback (como en el caso de
CallbackAction) o algo más complejo que se integre con arquitecturas completas
de deshacer/rehacer (por ejemplo) u otra lógica.
Los Shortcuts son vinculaciones de teclas que se activan al presionar una tecla o combinación
de teclas. Las combinaciones de teclas residen en una tabla con su intent vinculado. Cuando
el widget Shortcuts los invoca, envía su intent correspondiente al
subsistema de acciones para su cumplimiento.
Para ilustrar los conceptos en acciones y atajos, este artículo crea una app sencilla que permite a un usuario seleccionar y copiar texto en un campo de texto usando tanto botones como atajos.
¿Por qué separar Actions de Intents?
#Quizás te preguntes: ¿por qué no mapear directamente una combinación de teclas a una acción? ¿Por qué tener intents? Esto se debe a que es útil tener una separación de incumbencias entre dónde están las definiciones de mapeo de teclas (a menudo a un nivel alto), y dónde están las definiciones de acciones (a menudo a un nivel bajo), y porque es importante poder tener una sola combinación de teclas mapeada a una operación pretendida en una app, y hacer que se adapte automáticamente a cualquiera que sea la acción que cumple esa operación pretendida para el contexto enfocado.
Por ejemplo, Flutter tiene un widget ActivateIntent que mapea cada tipo de
control a su versión correspondiente de un ActivateAction (y que ejecuta
el código que activa el control). Este código a menudo necesita un acceso bastante privado
para hacer su trabajo. Si la capa adicional de indirección que proporcionan los Intents
no existiera, sería necesario elevar la definición de las acciones hacia
donde la instancia definitoria del widget Shortcuts pudiera verlas, haciendo
que los atajos tengan más conocimiento del necesario sobre qué acción
invocar, y tengan acceso o proporcionen un State que no necesariamente tendrían
o necesitarían de otro modo. Esto permite que tu código separe las dos preocupaciones para ser más
independiente.
Los intents configuran una acción para que la misma acción pueda servir para múltiples usos. Un
ejemplo de esto es DirectionalFocusIntent, que toma una dirección para mover
el foco, lo que permite que el DirectionalFocusAction sepa hacia qué dirección
mover el foco. Solo ten cuidado: no pases State en el Intent que se aplique
a todas las invocaciones de un Action: ese tipo de State debe pasarse al
constructor del propio Action, para evitar que el Intent necesite saber
demasiado.
¿Por qué no usar callbacks?
#También podrías preguntarte: ¿por qué no usar simplemente un callback en lugar de un objeto Action?
La razón principal es que es útil que las acciones decidan si están
habilitadas implementando isEnabled. Además, a menudo es de gran ayuda si las vinculaciones
de teclas, y la implementación de esas vinculaciones, están en diferentes lugares.
Si todo lo que necesitas son callbacks sin la flexibilidad de Actions y
Shortcuts, puedes usar el widget CallbackShortcuts:
@override
Widget build(BuildContext context) {
return CallbackShortcuts(
bindings: <ShortcutActivator, VoidCallback>{
const SingleActivator(LogicalKeyboardKey.arrowUp): () {
setState(() => count = count + 1);
},
const SingleActivator(LogicalKeyboardKey.arrowDown): () {
setState(() => count = count - 1);
},
},
child: Focus(
autofocus: true,
child: Column(
children: <Widget>[
const Text('Press the up arrow key to add to the counter'),
const Text('Press the down arrow key to subtract from the counter'),
Text('count: $count'),
],
),
),
);
}
Shortcuts
#Como verás a continuación, las acciones son útiles por sí solas, pero el caso de uso
más común implica vincularlas a un atajo de teclado. Para esto sirve el widget Shortcuts.
Se inserta en la jerarquía de widgets para definir combinaciones de teclas que
representan la intención del usuario cuando se presiona esa combinación de teclas. Para convertir
ese propósito pretendido para la combinación de teclas en una acción concreta, el
widget Actions se utiliza para mapear el Intent a un Action. Por ejemplo, puedes
definir un SelectAllIntent y vincularlo a tu propio SelectAllAction
o a tu
CanvasSelectAllAction, y a partir de esa única vinculación de tecla, el sistema invoca
cualquiera de los dos, dependiendo de qué parte de tu aplicación tenga el foco. Veamos cómo
funciona la parte de vinculación de teclas:
@override
Widget build(BuildContext context) {
return Shortcuts(
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyA):
const SelectAllIntent(),
},
child: Actions(
dispatcher: LoggingActionDispatcher(),
actions: <Type, Action<Intent>>{
SelectAllIntent: SelectAllAction(model),
},
child: Builder(
builder: (context) => TextButton(
onPressed: Actions.handler<SelectAllIntent>(
context,
const SelectAllIntent(),
),
child: const Text('SELECT ALL'),
),
),
),
);
}
El mapa proporcionado a un widget Shortcuts mapea un LogicalKeySet (o un
ShortcutActivator, consulta la nota a continuación) a una instancia de Intent. El conjunto de teclas lógicas
define un conjunto de una o más teclas, y el intent indica el propósito
pretendido de la pulsación de tecla. El widget Shortcuts busca pulsaciones de teclas en el mapa
para encontrar una instancia de Intent, la cual le entrega al método invoke()
de la acción.
El ShortcutManager
#El gestor de atajos (shortcut manager), un objeto de vida más larga que el widget Shortcuts, transmite
los eventos de teclado cuando los recibe. Contiene la lógica para decidir cómo
manejar las teclas, la lógica para subir por el árbol y encontrar otros mapeos de atajos,
y mantiene un mapa de combinaciones de teclas a intents.
Si bien el comportamiento predeterminado del ShortcutManager suele ser el deseado, el
widget Shortcuts acepta un ShortcutManager del cual puedes heredar para personalizar
su funcionalidad.
Por ejemplo, si quisieras registrar cada tecla que manejó un widget Shortcuts,
podrías crear un LoggingShortcutManager:
class LoggingShortcutManager extends ShortcutManager {
@override
KeyEventResult handleKeypress(BuildContext context, KeyEvent event) {
final KeyEventResult result = super.handleKeypress(context, event);
if (result == KeyEventResult.handled) {
print('Handled shortcut $event in $context');
}
return result;
}
}
Ahora, cada vez que el widget Shortcuts maneja un atajo, imprime el evento
de tecla y el contexto relevante.
Actions
#Las Actions permiten la definición de operaciones que la aplicación puede
realizar al invocarlas con un Intent. Las acciones se pueden habilitar o deshabilitar,
y reciben la instancia de intent que las invocó como argumento para permitir
su configuración por parte del intent.
Definiendo acciones
#Las acciones, en su forma más simple, son solo subclases de Action<Intent> con un
método invoke(). Aquí hay una acción simple que simplemente invoca una función en el
modelo proporcionado:
class SelectAllAction extends Action<SelectAllIntent> {
SelectAllAction(this.model);
final Model model;
@override
void invoke(covariant SelectAllIntent intent) => model.selectAll();
}
O, si da demasiada pereza crear una nueva clase, usa un CallbackAction:
CallbackAction(onInvoke: (intent) => model.selectAll());
Una vez que tienes una acción, la agregas a tu aplicación usando el widget Actions,
el cual toma un mapa de tipos de Intent a Actions:
@override
Widget build(BuildContext context) {
return Actions(
actions: <Type, Action<Intent>>{SelectAllIntent: SelectAllAction(model)},
child: child,
);
}
El widget Shortcuts utiliza el contexto del widget Focus y Actions.invoke
para
encontrar qué acción invocar. Si el widget Shortcuts no encuentra un tipo de
intent coincidente en el primer widget Actions encontrado, considera el siguiente
widget Actions ancestro, y así sucesivamente, hasta llegar a la raíz del árbol
de widgets, o encontrar un tipo de intent coincidente e invocar la acción correspondiente.
Invocando Actions
#El sistema de acciones tiene varias formas de invocar acciones. Con diferencia, la forma
más común es mediante el uso de un widget Shortcuts cubierto en la sección anterior,
pero existen otras formas de interrogar al subsistema de acciones e invocar una
acción. Es posible invocar acciones que no están vinculadas a teclas.
Por ejemplo, para encontrar una acción asociada con un intent, puedes usar:
Action<SelectAllIntent>? selectAll = Actions.maybeFind<SelectAllIntent>(
context,
);
Esto devuelve un Action asociado con el tipo SelectAllIntent si hay uno
disponible en el context dado. Si no hay uno disponible, devuelve null. Si
un Action asociado siempre debería estar disponible, entonces usa find
en lugar de
maybeFind, el cual lanza una excepción cuando no encuentra un tipo de
Intent
coincidente.
Para invocar la acción (si existe), llama a:
Object? result;
if (selectAll != null) {
result = Actions.of(
context,
).invokeAction(selectAll, const SelectAllIntent());
}
Combina eso en una sola llamada con lo siguiente:
Object? result = Actions.maybeInvoke<SelectAllIntent>(
context,
const SelectAllIntent(),
);
A veces quieres invocar una acción como
resultado de presionar un botón u otro control.
Puedes hacer esto con la función Actions.handler.
Si el intent tiene un mapeo a una acción habilitada,
la función Actions.handler crea un closure manejador.
Sin embargo, si no tiene un mapeo, devuelve null.
Esto permite que el botón se deshabilite si
no hay una acción habilitada que coincida en el contexto.
@override
Widget build(BuildContext context) {
return Actions(
actions: <Type, Action<Intent>>{SelectAllIntent: SelectAllAction(model)},
child: Builder(
builder: (context) => TextButton(
onPressed: Actions.handler<SelectAllIntent>(
context,
SelectAllIntent(controller: controller),
),
child: const Text('SELECT ALL'),
),
),
);
}
El widget Actions solo invoca acciones cuando isEnabled(Intent intent)
devuelve true, permitiendo que la acción decida si el despachador debe considerarla
para su invocación. Si la acción no está habilitada, la acción no se invoca y el
framework deja de buscar acciones coincidentes en ese punto.
El ejemplo anterior usa un Builder porque Actions.handler y
Actions.invoke (por ejemplo) solo encuentran acciones en el context proporcionado, y
si el ejemplo pasa el context dado a la función build, el framework
comienza a buscar por encima del widget actual. El uso de un Builder permite que el
framework encuentre las acciones definidas en la misma función build.
Puedes invocar una acción sin necesitar un BuildContext, pero dado que el
widget Actions requiere un contexto para encontrar una acción habilitada a invocar, necesitas
proporcionar uno, ya sea creando tu propia instancia de Action, o
encontrando una en un contexto apropiado con Actions.find.
Para invocar la acción, pasa la acción al método invoke en un
ActionDispatcher, ya sea uno creado por ti mismo, o uno obtenido de un
widget Actions existente usando el método Actions.of(context). Comprueba si
la acción está habilitada antes de llamar a invoke. Por supuesto, también puedes llamar a
invoke en la propia acción, pasando un Intent, pero entonces te estás excluyendo de cualquier
servicio que un despachador de acciones pueda proporcionar (como registro, deshacer/rehacer, y
demás).
Despachadores de acciones
#La mayor parte del tiempo, solo quieres invocar una acción, hacer que realice su trabajo y olvidarte de ella. A veces, sin embargo, es posible que desees registrar las acciones ejecutadas.
Aquí es donde entra el reemplazo del ActionDispatcher predeterminado por un despachador personalizado.
Pasas tu ActionDispatcher al widget Actions,
y este
invoca acciones de cualquier widget Actions por debajo de ese que no defina un
despachador propio.
Lo primero que hace Actions al invocar una acción es buscar el
ActionDispatcher y pasarle la acción para su invocación. Si no hay ninguno,
crea un ActionDispatcher predeterminado que simplemente invoca la acción.
Sin embargo, si deseas un registro de todas las acciones invocadas, puedes crear tu propio
LoggingActionDispatcher para hacer el trabajo:
class LoggingActionDispatcher extends ActionDispatcher {
@override
Object? invokeAction(
covariant Action<Intent> action,
covariant Intent intent, [
BuildContext? context,
]) {
print('Action invoked: $action($intent) from $context');
super.invokeAction(action, intent, context);
return null;
}
@override
(bool, Object?) invokeActionIfEnabled(
covariant Action<Intent> action,
covariant Intent intent, [
BuildContext? context,
]) {
print('Action invoked: $action($intent) from $context');
return super.invokeActionIfEnabled(action, intent, context);
}
}
Luego le pasas eso a tu widget Actions de nivel superior:
@override
Widget build(BuildContext context) {
return Actions(
dispatcher: LoggingActionDispatcher(),
actions: <Type, Action<Intent>>{SelectAllIntent: SelectAllAction(model)},
child: Builder(
builder: (context) => TextButton(
onPressed: Actions.handler<SelectAllIntent>(
context,
const SelectAllIntent(),
),
child: const Text('SELECT ALL'),
),
),
);
}
Esto registra cada acción a medida que se ejecuta, así:
flutter: Action invoked: SelectAllAction#906fc(SelectAllIntent#a98e3) from Builder(dependencies: _[ActionsMarker])
Juntando todo
#La combinación de Actions y Shortcuts es potente: puedes definir intents
genéricos que se mapean a acciones específicas a nivel de widget. Aquí hay una app sencilla
que ilustra los conceptos descritos anteriormente. La app crea un campo de texto que
también tiene botones de "seleccionar todo" y "copiar al portapapeles" junto a él. Los botones
invocan acciones para realizar su trabajo. Todas las acciones e
atujos invocados quedan registrados.
A menos que se indique lo contrario, la documentación en este sitio refleja Flutter 3.44.0. Página actualizada por última vez el 26-06-2026. Ver código fuente oreportar un problema.