Revisión de la API de Actions
Elimina la necesidad de FocusNode en las invocaciones, mapea tipos de Intent a Actions.
Resumen
#En Flutter, un Intent
es un objeto que habitualmente se vincula
a una combinación de teclas del teclado usando el Widget Shortcuts
.
Un Intent se puede vincular a una Action,
que puede actualizar el State de la aplicación o realizar otras operaciones.
En el transcurso del uso de esta API, identificamos varios inconvenientes
en el diseño, por lo que hemos actualizado la API de Actions para hacerla más fácil
de usar y entender.
En el diseño anterior de la API de Actions, las acciones se mapeaban desde una
LocalKey
a un ActionFactory que creaba una nueva
Action cada vez que se llamaba al método invoke.
En la API actual, las acciones se mapean desde el tipo de Intent
a una instancia de Action (con un Map<Type, Action>),
y no se crean de nuevo para cada invocación.
Contexto
#El diseño original de la API de Actions estaba orientado a invocar acciones desde widgets, y a hacer que esas acciones actuaran en el contexto del widget. Los equipos han estado utilizando acciones y han encontrado varias limitaciones en ese diseño que debían abordarse:
-
Las acciones no se podían invocar desde fuera de la jerarquía de widgets. Ejemplos de esto incluyen el procesamiento de un script de comandos, algunas arquitecturas de deshacer (undo) y algunas arquitecturas de controladores.
-
El mapeo de la tecla de acceso rápido a
Intenty luego aActionno siempre era claro, ya que las estructuras de datos mapeaban LogicalKeySet => Intent y luegoLocalKey=>ActionFactory. El nuevo mapeo sigue siendoLogicalKeySetaIntent, pero luego mapeaType(tipo deIntent) aAction, lo cual es más directo y legible, dado que el tipo de intent está escrito en el mapeo. -
Si la combinación de teclas para una acción estaba en otra parte de la jerarquía de Widgets, no siempre era posible para el
Intenttener acceso al State necesario para decidir si el intent/acción debía habilitarse o no.
Para abordar estos problemas, realizamos algunos cambios significativos en la API.
El mapeo de acciones se hizo más intuitivo
y la interfaz de habilitación (enabled) se movió a la clase Action.
Se eliminaron algunos argumentos innecesarios del método invoke de Action
y de su constructor, y se permitió que las acciones
devolvieran resultados de su método invoke.
Las acciones se convirtieron en genéricas, aceptando el tipo de Intent
que manejan, y ya no se utilizaron LocalKeys para identificar
qué acción ejecutar, utilizándose en su lugar el tipo de Intent.
La mayoría de estos cambios se realizaron en los PR para Revise Action API y Make Action.enabled be isEnabled(Intent intent) instead, y se describen en detalle en el documento de diseño.
Descripción del cambio
#Aquí están los cambios realizados para abordar los problemas anteriores:
- El
Map<LocalKey, ActionFactory>que se le daba al widgetActionsahora es unMap<Type, Action<Intent>>(el tipo es el tipo de Intent que se pasará a la Action). - El método
isEnabledse movió de la claseIntenta la claseAction. - Se eliminó el argumento
FocusNodede los métodosAction.invokeyActions.invoke. - Invocar una acción ya no crea una nueva instancia de
Action. - Se eliminó el argumento
LocalKeydel constructor deIntent. - Se eliminó el argumento
LocalKeydeCallbackAction. - La clase
Actionahora es genérica (Action<T extends Intent>) para una mejor seguridad de tipos. - El
OnInvokeCallbackutilizado porCallbackActionya no toma un argumentoFocusNode. - La firma de
ActionDispatcher.invokeActionha cambiado para no aceptar unFocusNodeopcional, sino tomar unBuildContextopcional en su lugar. - Las constantes estáticas
LocalKey(llamadas key por convención) en las subclases deActionhan sido eliminadas. - Los métodos
Action.invokeyActionDispatcher.invokeActionahora devuelven el resultado de invocar la acción como unObject. - La clase
Actionahora puede ser escuchada para cambios de estado. - El typedef
ActionFactoryha sido eliminado, ya que ya no se utiliza.
Ejemplos de fallos del analizador
#Aquí hay algunos ejemplos de fallos del analizador que podrían encontrarse cuando un uso desactualizado de la API de Actions podría ser la causa del problema. Los detalles del error pueden diferir y puede haber otros fallos causados por estos cambios.
error: MyActionDispatcher.invokeAction' ('bool Function(Action<Intent>, Intent, {FocusNode focusNode})') isn't a valid override of 'ActionDispatcher.invokeAction' ('Object Function(Action<Intent>, Intent, [BuildContext])'). (invalid_override at [main] lib/main.dart:74)
error: MyAction.invoke' ('void Function(FocusNode, Intent)') isn't a valid override of 'Action.invoke' ('Object Function(Intent)'). (invalid_override at [main] lib/main.dart:231)
error: The method 'isEnabled' isn't defined for the type 'Intent'. (undefined_method at [main] lib/main.dart:97)
error: The argument type 'Null Function(FocusNode, Intent)' can't be assigned to the parameter type 'Object Function(Intent)'. (argument_type_not_assignable at [main] lib/main.dart:176)
error: The getter 'key' isn't defined for the type 'NextFocusAction'. (undefined_getter at [main] lib/main.dart:294)
error: The argument type 'Map<LocalKey, dynamic>' can't be assigned to the parameter type 'Map<Type, Action<Intent>>'. (argument_type_not_assignable at [main] lib/main.dart:418)
Guía de migración
#Se requieren cambios significativos para actualizar el código existente a la nueva API.
Mapeo de acciones para acciones predefinidas
#Para actualizar los mapas de acciones en el widget Actions para
acciones predefinidas en Flutter, como ActivateAction
y SelectAction, haz lo siguiente:
- Actualiza el tipo de argumento del argumento
actions - Usa una instancia de una clase
Intentespecífica en el mapeo deShortcuts, en lugar de una instancia deIntent(TheAction.key).
Código antes de la migración:
class MyWidget extends StatelessWidget {
// ...
@override
Widget build(BuildContext context) {
return Shortcuts(
shortcuts: <LogicalKeySet, Intent> {
LogicalKeySet(LogicalKeyboardKey.enter): Intent(ActivateAction.key),
},
child: Actions(
actions: <LocalKey, ActionFactory>{
Activate.key: () => ActivateAction(),
},
child: Container(),
)
);
}
}
Código después de la migración:
class MyWidget extends StatelessWidget {
// ...
@override
Widget build(BuildContext context) {
return Shortcuts(
shortcuts: <LogicalKeySet, Intent> {
LogicalKeySet(LogicalKeyboardKey.enter): ActivateIntent,
},
child: Actions(
actions: <Type, Action<Intent>>{
ActivateIntent: ActivateAction(),
},
child: Container(),
)
);
}
}
Acciones personalizadas
#Para migrar tus acciones personalizadas, elimina las LocalKeys
que hayas definido y reemplázalas con subclases de Intent,
así como cambiar el tipo de argumento del argumento actions
del widget Actions.
Código antes de la migración:
class MyAction extends Action {
MyAction() : super(key);
/// The [LocalKey] that uniquely identifies this action to an [Intent].
static const LocalKey key = ValueKey<Type>(RequestFocusAction);
@override
void invoke(FocusNode node, MyIntent intent) {
// ...
}
}
class MyWidget extends StatelessWidget {
// ...
@override
Widget build(BuildContext context) {
return Shortcuts(
shortcuts: <LogicalKeySet, Intent> {
LogicalKeySet(LogicalKeyboardKey.enter): Intent(MyAction.key),
},
child: Actions(
actions: <LocalKey, ActionFactory>{
MyAction.key: () => MyAction(),
},
child: Container(),
)
);
}
}
Código después de la migración:
// You may need to create new Intent subclasses if you used
// a bare LocalKey before.
class MyIntent extends Intent {
const MyIntent();
}
class MyAction extends Action<MyIntent> {
@override
Object invoke(MyIntent intent) {
// ...
}
}
class MyWidget extends StatelessWidget {
// ...
@override
Widget build(BuildContext context) {
return Shortcuts(
shortcuts: <LogicalKeySet, Intent> {
LogicalKeySet(LogicalKeyboardKey.enter): MyIntent,
},
child: Actions(
actions: <Type, Action<Intent>>{
MyIntent: MyAction(),
},
child: Container(),
)
);
}
}
Acciones e Intents personalizados con argumentos
#
Para actualizar las acciones que usan argumentos de intent o mantienen un estado,
necesitas modificar los argumentos del método invoke.
En el ejemplo a continuación, el código mantiene el valor del
argumento en el intent como parte de la instancia de la acción.
Esto se debe a que en el diseño antiguo se creaba una nueva instancia
de la acción cada vez que se ejecutaba,
y el ActionDispatcher
podía conservar la acción resultante para registrar el estado.
En el ejemplo de código posterior a la migración a continuación,
la nueva MyAction devuelve el estado como resultado
de llamar a invoke, ya que no se crea una nueva instancia
para cada invocación. Este estado se devuelve al llamador de
Actions.invoke, o ActionDispatcher.invokeAction,
dependiendo de cómo se invoque la acción.
Código antes de la migración:
class MyIntent extends Intent {
const MyIntent({this.argument});
final int argument;
}
class MyAction extends Action {
MyAction() : super(key);
/// The [LocalKey] that uniquely identifies this action to an [Intent].
static const LocalKey key = ValueKey<Type>(RequestFocusAction);
int state;
@override
void invoke(FocusNode node, MyIntent intent) {
// ...
state = intent.argument;
}
}
Código después de la migración:
class MyIntent extends Intent {
const MyIntent({this.argument});
final int argument;
}
class MyAction extends Action<MyIntent> {
@override
int invoke(Intent intent) {
// ...
return intent.argument;
}
}
Timeline
#Introducido en la versión: 1.18
En la versión estable: 1.20
Referencias
#Documentación de la API:
Problema relevante:
PRs relevantes:
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-05. Ver código fuente oreportar un problema.