概述

#

在 Flutter 中,Intent 是一個通常使用 Shortcuts widget 繫結到鍵盤組合鍵的物件。Intent 可以繫結到 Action,該 Action 可以更新應用程式狀態或執行其他操作。在使用此 API 的過程中,我們發現設計存在一些弊端,因此我們更新了 Actions API,使其更易於使用和理解。

在之前的 Actions API 設計中,操作是從 LocalKey 對映到 ActionFactory,該 ActionFactory 在每次呼叫 invoke 方法時都會建立一個新的 Action。在當前 API 中,操作是從 Intent 的型別對映到 Action 例項(使用 Map<Type, Action>),並且不會為每次呼叫都重新建立。

背景

#

最初的 Actions API 設計側重於從 widget 呼叫操作,並讓這些操作在 widget 的上下文中執行。團隊一直在使用操作,並發現該設計存在一些需要解決的限制。

  1. 操作無法從 widget 樹外部呼叫。例如,處理命令指令碼、某些撤銷架構和某些控制器架構。

  2. 從快捷鍵到 Intent 再到 Action 的對映並不總是清晰的,因為資料結構對映為 LogicalKeySet =>Intent,然後是 LocalKey => ActionFactory。新的對映仍然是 LogicalKeySetIntent,但現在它將 TypeIntent 型別)對映到 Action,這更直接、更易讀,因為意圖的型別寫在對映中。

  3. 如果某個操作的鍵繫結位於 widget 樹的另一部分,則 Intent 並非總是能夠訪問必要的狀態來決定該意圖/操作是否應該啟用。

為了解決這些問題,我們對 API 進行了一些重大更改。操作的對映更加直觀,並且 enabled 介面已移至 Action 類。從 Actioninvoke 方法及其建構函式中移除了不必要的引數,並允許操作從其 invoke 方法返回結果。操作已改為泛型,接受它們處理的 Intent 型別,並且不再使用 LocalKeys 來標識要執行的操作,而是使用 Intent 的型別。

這些更改大部分是在 Revise Action APIMake Action.enabled be isEnabled(Intent intent) instead 的 PR 中完成的,並在 設計文件 中進行了詳細描述。

變更說明

#

以下是為解決上述問題所做的更改:

  1. 傳遞給 Actions widget 的 Map<LocalKey, ActionFactory> 現在是 Map<Type, Action<Intent>>(型別是傳遞給 Action 的 Intent 的型別)。
  2. isEnabled 方法已從 Intent 類移至 Action 類。
  3. 已從 Action.invokeActions.invoke 方法中移除了 FocusNode 引數。
  4. 呼叫操作不再建立 Action 的新例項。
  5. 已從 Intent 建構函式中移除了 LocalKey 引數。
  6. 已從 CallbackAction 中移除了 LocalKey 引數。
  7. Action 類現在是一個泛型(Action<T extends Intent>),以提高型別安全性。
  8. CallbackAction 使用的 OnInvokeCallback 不再接受 FocusNode 引數。
  9. ActionDispatcher.invokeAction 的簽名已更改為不接受可選的 FocusNode,而是接受可選的 BuildContext
  10. 已移除 Action 子類中(按約定命名為 key)的 LocalKey 靜態常量。
  11. Action.invokeActionDispatcher.invokeAction 方法現在將呼叫操作的結果作為 Object 返回。
  12. 現在可以監聽 Action 類以檢視狀態更改。
  13. ActionFactory 型別定義已被移除,因為它不再使用。

示例分析器故障

#

以下是一些可能遇到的示例分析器故障,其中過時地使用 Actions API 可能是問題的原因。錯誤的具體細節可能有所不同,並且可能還存在由這些更改引起的其他故障。

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)

遷移指南

#

需要進行重大的更改才能將現有程式碼更新到新 API。

預定義操作的對映

#

要更新 Actions widget 中 Flutter 預定義操作(如 ActivateActionSelectAction)的操作對映,請執行以下操作:

  • 更新 actions 引數的引數型別。
  • Shortcuts 對映中使用特定 Intent 類的例項,而不是 Intent(TheAction.key) 例項。

遷移前的程式碼

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(),
      )
    );
  }
}

遷移後的程式碼

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(),
      )
    );
  }
}

自定義操作

#

要遷移自定義操作,請刪除已定義的 LocalKeys,並將其替換為 Intent 子類,以及更改 Actions widget 的 actions 引數的引數型別。

遷移前的程式碼

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(),
      )
    );
  }
}

遷移後的程式碼

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(),
      )
    );
  }
}

帶有引數的自定義 ActionsIntents

#

要更新使用意圖引數或持有狀態的操作,您需要修改 invoke 方法的引數。在下面的示例中,程式碼將意圖中的引數值保留為操作例項的一部分。這是因為在舊的設計中,每次執行操作時都會建立一個新例項,並且 ActionDispatcher 可以保留結果操作以記錄狀態。

在下面的遷移後代碼示例中,新的 MyAction 將狀態作為呼叫 invoke 的結果返回,因為每次呼叫都不會建立新例項。此狀態將返回給呼叫 Actions.invokeActionDispatcher.invokeAction 的呼叫者,具體取決於操作的呼叫方式。

遷移前的程式碼

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;
  }
}

遷移後的程式碼

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;
  }
}

時間線

#

已釋出到版本:1.18
穩定版本中:1.20

參考資料

#

API 文件

相關議題

相關 PR