Actions API 修訂
移除呼叫時需傳入 FocusNode,並將 Intent 類型對應至 Actions。
摘要
#
在 Flutter 中,Intent
是一個物件,通常會透過 Shortcuts
元件 (Widget) 綁定至某個鍵盤組合鍵。
Intent 可以綁定至 Action,
用於更新應用程式狀態或執行其他操作。
在使用此 API 的過程中,我們發現設計上有幾個缺點,因此我們對 Actions API 進行了更新,使其更易於使用與理解。
在先前的 Actions API 設計中,actions 是從 LocalKey
對應到 ActionFactory,每次呼叫 invoke 方法時都會建立一個新的 Action。
在目前的 API 中,actions 是從 Intent 的類型對應到 Action 實例(帶有 Map<Type, Action>),
且每次呼叫時不會重新建立實例。
背景說明
#原本的 Actions API 設計是為了讓 actions 能從元件 (Widget) 中被呼叫,並在該元件的上下文中執行。 團隊在實際使用 actions 時,發現該設計有幾項限制需要改進:
-
無法從元件 (Widget) 階層外部呼叫 actions。 例如:處理指令腳本、某些 undo 架構,以及部分 controller 架構。
-
從快捷鍵到
Intent再到Action的對應關係並不總是很清楚,因為資料結構是將 LogicalKeySet => Intent,再由LocalKey=>ActionFactory。新的對應方式仍然是LogicalKeySet對應Intent,但接著會將Type(Intent類型)對應到Action, 這樣的設計更直接且易讀,因為 intent 的類型會直接寫在對應表中。 如果某個 action 的鍵盤綁定在元件階層的其他地方,
Intent不一定能取得決定 intent/action 是否啟用所需的狀態。
為了解決這些問題,我們對 API 進行了重大調整。
actions 的對應方式變得更直觀,
而啟用(enabled)介面則移至 Action 類別。
另外,Action 的 invoke 方法與建構函式中也移除了不必要的參數,並允許 actions 的 invoke 方法回傳結果。
actions 現在改為泛型(generics),可指定所處理的 Intent 類型,且不再使用 LocalKeys
來識別要執行哪個 action,而是改用 Intent 的類型。
這些主要變更大多來自 Revise Action API 及 Make Action.enabled be isEnabled(Intent intent) instead 這兩個 PR,詳細內容可參考設計文件。
變更說明
#以下是針對上述問題所做的調整:
-
提供給
Actions元件 (Widget) 的Map<LocalKey, ActionFactory>,現在是一個Map<Type, Action<Intent>>(其類型為要傳遞給 Action 的 Intent 類型)。 isEnabled方法已從Intent類別移至Action類別。Action.invoke與Actions.invoke方法中的FocusNode參數已被移除。- 呼叫 action 時不再會建立新的
Action實例。 Intent建構函式中的LocalKey參數已被移除。CallbackAction的LocalKey參數已被移除。Action類別現在為泛型(Action<T extends Intent>),以提升型別安全性。CallbackAction所使用的OnInvokeCallback不再需要FocusNode參數。-
ActionDispatcher.invokeAction的簽章已變更,不再接受可選的FocusNode,而是改為可選的BuildContext。 Action子類別中的LocalKey靜態常數(依慣例命名為 key)已被移除。-
Action.invoke與ActionDispatcher.invokeAction方法現在會以Object形式回傳 action 執行的結果。 Action類別現在可以監聽狀態變化。ActionFactorytypedef 已被移除,因為已不再使用。
範例分析器錯誤
#以下是一些可能因為 Actions API 使用過時而導致的分析器(analyzer)錯誤範例。實際錯誤內容可能有所不同,且這些變更也可能導致其他錯誤。
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 對應
#
若要在 Actions 元件 (Widget) 中,針對 Flutter 的預設動作(例如 ActivateAction 和 SelectAction)更新 action 對應,請依下列步驟操作:
- 更新
actions參數的參數型別 -
在
Shortcuts對應中,使用特定Intent類別的實例,而非Intent(TheAction.key)實例。
遷移前的程式碼:
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(),
)
);
}
}
遷移後的程式碼:
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(),
)
);
}
}
自訂 actions
#
若要遷移您的自訂 actions,請移除您所定義的 LocalKeys,並以 Intent 子類別取代,同時也要將 Actions
元件的 actions 參數型別進行相應更改。
遷移前的程式碼:
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(),
)
);
}
}
遷移後的程式碼:
// 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(),
)
);
}
}
帶有參數的自訂 Actions 和 Intents
#
若要更新使用 intent 參數或保存狀態的 action,
你需要修改傳遞給 invoke 方法的參數。
在下方的範例中,程式碼會將參數的值保存在 intent 中,作為 action 實例的一部分。
這是因為在舊的設計中,每次執行時都會建立新的 action 實例,
而產生的 action 可能會被 ActionDispatcher
保留,以記錄狀態。
在下方遷移後的程式碼範例中,
新的 MyAction 會在呼叫 invoke 時,將狀態作為結果回傳,
因為現在每次呼叫不會再建立新的實例。
這個狀態會回傳給 Actions.invoke 或 ActionDispatcher.invokeAction 的呼叫者,
具體取決於 action 的呼叫方式。
遷移前的程式碼:
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;
}
}
遷移後的程式碼:
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:
Unless stated otherwise, the documentation on this site reflects Flutter 3.44.0. Page last updated on 2026-06-14. View source or report an issue.