Saltar al contenido principal

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:

  1. 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.

  2. El mapeo de la tecla de acceso rápido a Intent y luego a Action no siempre era claro, ya que las estructuras de datos mapeaban LogicalKeySet => Intent y luego LocalKey => ActionFactory. El nuevo mapeo sigue siendo LogicalKeySet a Intent, pero luego mapea Type (tipo de Intent) a Action, lo cual es más directo y legible, dado que el tipo de intent está escrito en el mapeo.

  3. 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 Intent tener 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:

  1. El Map<LocalKey, ActionFactory> que se le daba al widget Actions ahora es un Map<Type, Action<Intent>> (el tipo es el tipo de Intent que se pasará a la Action).
  2. El método isEnabled se movió de la clase Intent a la clase Action.
  3. Se eliminó el argumento FocusNode de los métodos Action.invoke y Actions.invoke.
  4. Invocar una acción ya no crea una nueva instancia de Action.
  5. Se eliminó el argumento LocalKey del constructor de Intent.
  6. Se eliminó el argumento LocalKey de CallbackAction.
  7. La clase Action ahora es genérica (Action<T extends Intent>) para una mejor seguridad de tipos.
  8. El OnInvokeCallback utilizado por CallbackAction ya no toma un argumento FocusNode.
  9. La firma de ActionDispatcher.invokeAction ha cambiado para no aceptar un FocusNode opcional, sino tomar un BuildContext opcional en su lugar.
  10. Las constantes estáticas LocalKey (llamadas key por convención) en las subclases de Action han sido eliminadas.
  11. Los métodos Action.invoke y ActionDispatcher.invokeAction ahora devuelven el resultado de invocar la acción como un Object.
  12. La clase Action ahora puede ser escuchada para cambios de estado.
  13. El typedef ActionFactory ha 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 Intent específica en el mapeo de Shortcuts, en lugar de una instancia de Intent(TheAction.key).

Código antes de la migración:

dart
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:

dart
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:

dart
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:

dart
// 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:

dart
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:

dart
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: