Flutter Quick Actions 多動作處理:可擴展程式架構設計與實作步驟

Published on: | Last updated:

Flutter Quick Actions 筆記

今天來聊聊 Flutter 的 `quick_actions`。這東西…很多人都在用,就是長按 App icon 跳出來的快捷鍵。很方便。

但 code 很容易寫爛。一堆 if-else 或 switch-case 塞在同一個地方。不好維護。

這篇是整理一個比較乾淨的架構。一個…嗯,筆記。

重點一句話

老實說,就是用 Command Pattern。把每個 action 都包成獨立的 class,不要寫在一起。

為什麼不要只用 if-else?

一開始可能很快,兩個 action,`if (action == 'home')`... `else if (action == 'settings')`... 搞定。

但之後呢?加第三個、第四個...那個 `if` 區塊會變超長。每次要加新功能,就要去動那個最核心的判斷邏輯。很危險。

說真的,我自己是覺得,架構的目的就是為了讓未來的修改「更安全」、「更獨立」。改 A 不會動到 B。

這個想法,可以看一下 Android Developers 官方對 App Shortcuts 的文件,他們雖然沒規定你 code 怎麼寫,但設計理念就是每個捷徑都是一個獨立的意圖(intent)。我們的 code 也應該反映這點。在 iOS 上,雖然叫法是 `ShortcutItem`,但概念差不多。

視覺化的 App 快捷功能
視覺化的 App 快捷功能

實作指引

好,所以整個架構怎麼拆。大概分三塊東西:Client、Action、Navigator。

1. QuickActionsClient:總管

這東西是老大。負責跟 `quick_actions` 套件直接溝通。初始化、設定要顯示哪些按鈕、清掉按鈕,都他做。

這裡面有個小技巧,但超級重要。


abstract interface class QuickActionsClient {
  factory QuickActionsClient.instance() => _QuickActionsClientImpl();

  Future<void> init();
  void checkAndNavigate();
  Future<void> setShortcutItems(/* ... */);
  Future<void> clearShortcutItems();
}

final class _QuickActionsClientImpl implements QuickActionsClient {
  final QuickActions _quickActions = const QuickActions();

  late DateTime initTime;
  late DateTime actionTime;
  QuickActionClientType? lastAction;
  QuickActionClientType? initAction;

  final threshold = 2; // Seconds

  @override
  Future<void> init() async {
    initTime = DateTime.now();
    await _quickActions.initialize((String newActionStr) {
      // ... action handling logic ...
      final newAction = QuickActionClientType.fromString(newActionStr);
      if (newAction != null) {
        actionTime = DateTime.now();
        lastAction = newAction;
        _checkLastAction(newAction);
      }
    });
  }

  void _checkLastAction(QuickActionClientType newAction) {
    final diff = actionTime.difference(initTime).inSeconds;
    if (diff < threshold) {
      // App 是因為點擊 quick action 而啟動的
      initAction = newAction;
    } else {
      // App 已經在前景,使用者點了 quick action
      PlatformQuickActionNavigator.instance.execute(newAction, isInitial: false);
    }
  }

  @override
  void checkAndNavigate() {
    // 這個在 App 啟動後才呼叫
    if (initAction != null) {
      PlatformQuickActionNavigator.instance.execute(initAction!, isInitial: true);
      initAction = null; // 用完就清掉
    }
  }

  @override
  Future<void> setShortcutItems(/* ... */) async {
    await _quickActions.setShortcutItems(
      <ShortcutItem>[
        ShortcutItem(
          type: QuickActionClientType.home.name,
          localizedTitle: '首頁',
          icon: 'quick_actions_icon',
        ),
        ShortcutItem(
          type: QuickActionClientType.settings.name,
          localizedTitle: '設定',
          icon: 'quick_actions_icon',
        ),
      ],
    );
  }

  // ... clearShortcutItems() ...
}
    </void></void></void></void></void>

看到了嗎?`_checkLastAction` 裡面那個時間差判斷。用 `initTime` 和 `actionTime` 的秒差,去猜這次的 action 是不是「跟著 App 一起啟動」的。

這很關鍵。因為「啟動 App 並跳轉」跟「在已開啟的 App 內跳轉」的處理方式,有時候會不一樣。例如,前者可能需要等某些服務都初始化完畢才能跳,後者可以直接跳。

2. IPlatformQuickAction:指令本人

這是一個 interface,一個規格。所有 Quick Action 的行為都要遵照這個規格來做。

這就是 Command Pattern 的核心。每個指令都是一個物件。


abstract interface class IPlatformQuickAction {
  // 判斷這個指令是不是我要處理的 type
  bool isSatisfied(QuickActionClientType type);

  // 執行具體動作
  Future<void> execute({required bool isInitial});
}
    </void>

然後我們就為 `home` 和 `settings` 各做一個 class。


// 前往首頁的 Action
final class HomePlatformQuickAction implements IPlatformQuickAction {
  @override
  bool isSatisfied(QuickActionClientType type) => type == QuickActionClientType.home;

  @override
  Future<void> execute({required bool isInitial}) async {
    // 這裡就是你的跳轉邏輯
    unawaited(App.router.push(const HomeGuardRoute()));
  }
}

// 前往設定頁的 Action
final class SettingsPlatformQuickAction implements IPlatformQuickAction {
  @override
  bool isSatisfied(QuickActionClientType type) => type == QuickActionClientType.settings;

  @override
  Future<void> execute({required bool isInitial}) async {
    unawaited(App.router.push(const SettingsGuardRoute()));
  }
}
    </void></void>

很乾淨。一個檔案只做一件事。要加第三個 `Profile` action?就再新增一個 `profile_action.dart` 檔案。完全不用改到舊的 code。

程式碼檔案結構示意
程式碼檔案結構示意

3. PlatformQuickActionNavigator:交通警察

最後是這個 Navigator。他手上有一份清單,記載了所有我們做好的 Action(`HomePlatformQuickAction`、`SettingsPlatformQuickAction` 等等)。

他的工作很單純:當 `Client` 跟他說「嘿,有個 `settings` action 進來了」,他就去清單裡一個一個問:「這是你要處理的嗎?」 (`isSatisfied`),直到找到對的人,然後叫他去 `execute`。


final class PlatformQuickActionNavigator {
  PlatformQuickActionNavigator._();
  static final PlatformQuickActionNavigator instance = PlatformQuickActionNavigator._();

  // 把所有做好的 Action 都註冊在這裡
  final items = <IPlatformQuickAction>[
    HomePlatformQuickAction(),
    SettingsPlatformQuickAction(),
  ];

  Future<void> execute(QuickActionClientType type, {required bool isInitial}) async {
    // 找到對應的 action 然後執行它
    await items.firstWhereOrNull((element) => element.isSatisfied(type))?.execute(isInitial: isInitial);
  }
}
    </void>
指令模式 (Command Pattern) 的運作流程
指令模式 (Command Pattern) 的運作流程

兩種作法的比較

所以...繞這麼一大圈,跟直接用 `switch-case` 到底差在哪?

評估項目 傳統 Switch-Case Command Pattern 架構
擴充性 差。每次加新 action 都要動到那個超大的 switch block。 好。新增一個 action 檔就好,完全不碰舊 code。符合開閉原則。
可讀性 糟。所有邏輯混在一起,當 action 一多,根本是災難。 優。一個檔案只負責一件事,非常清楚。檔名就說明了一切。
職責分離 沒有分離。一個巨大的函式包辦了所有事。 非常清楚。Client 管監聽,Navigator 管派發,Action 管執行。
測試 難。很難針對單一 action 寫單元測試。 簡單。每個 Action class 都可以獨立拿出來測試。

常見錯誤與修正

我自己是覺得,這個架構最容易出錯的地方,是導航 (Navigation)。

特別是 `isInitial: true` 的時候,也就是 App 因為 quick action 才啟動。這時候你的 `BuildContext` 可能還沒準備好,或是 `Navigator` 還不能用。

如果在 `execute` 裡面直接 `Navigator.of(context).push(...)`,很可能會 crash。

解決方法... 嗯,一種是確保你的 `App.router` (或任何你用的路由工具) 是全局的 (global),不需要 `context` 就能跳轉。另一種方法是把 `initAction` 存起來,等到 App 的主頁面 `didChangeDependencies` 或類似的生命週期被觸發時,再檢查並執行這個被存起來的 action。原文的 `checkAndNavigate()` 就是在做這件事。

結語

差不多是這樣。這個模式不是什麼新發明,就是設計模式的應用而已。但用在 Quick Actions 上,真的蠻酷的,可以讓 code 變得非常乾淨、好管理。

重點不在 `class` 叫什麼名字,而是那個「拆分」的精神。

你還在用 if-else 或 switch-case 管理你的 Quick Actions 嗎?還是你有更乾淨的作法?留言分享一下你的思路吧。

Related to this topic:

Comments

  1. Guest 2025-07-22 Reply
    嗯...這篇文章看起來頗專業的樣子。不過我有點好奇,快速動作真的那麼重要嗎?感覺像是又一個開發者的小把戲。不過,聽起來好像很有趣的樣子,想再多了解一下。
撥打專線 LINE免費通話