搜尋套件、儲存庫與 APP 分類

套件詳情v1.5.0

flutter_assistant_intents

透過類型化 Dart 處理程式,將 task-app 操作公開給語音助理——iOS App Intents(Siri、Spotlight、Shortcuts)與 Android 應用捷徑。

AndroidiOS
近 30 天下載
1.3萬
喜歡數
2
Pub 分數
160

安裝套件

flutter pub add flutter_assistant_intents

套件介紹

根據套件公開資料產生的選型評估,僅供參考。

flutter_assistant_intents 讓 Flutter 應用能夠與 iOS(Siri、Spotlight、Shortcuts)和 Android 上的語音助理及系統捷徑整合。它提供類型化的 Dart 處理常式,用於應用動作,讓使用者能透過語音指令或快速存取捷徑觸發特定任務。此套件透過抽象平台特定的實作細節,簡化深度連結,確保兩大平台上的行為一致。開發者可定義帶有參數與回應的意圖動作,輕鬆透過自然語言與手勢互動來公開應用功能。

適用情境

  • 透過Siri和Google助理啟用語音激活的應用程式動作
  • 在 iOS Spotlight 和 Android 快速設定中建立情境感知的捷徑
  • 透過應用捷徑支援任務自動化
  • 使用自然語言命令與使用者工作流程整合
  • 允許從系統級介面實現無縫深度連結
  • 建構跨平台助手相容功能

主要特色

  • 用於應用意圖的類型化 Dart 處理程序
  • 跨平台支援 iOS 和 Android
  • 與 App Intents 和 Android 快捷方式的整合
  • 自動參數處理和驗證
  • 內建支援動態捷徑更新
  • 用於定義動作的簡單宣告式 API

適合對象

  • 開發支援語音功能的 Flutter 應用程式的開發者
  • 需要與系統級捷徑深度整合的應用程式
  • 同時針對 iOS 和 Android 的團隊,共享邏輯
  • 需要通過語音或手勢實現任務自動化的專案
  • 從上下文搜尋和發現中受益的應用程式

使用建議

  • 需要超出Flutter代碼的平台特定設定
  • 僅限支援的平台(iOS 14+,Android 6+)
  • 可能需要在應用清單中新增額外的元資料
  • 不適用於非互動式背景任務
  • 依賴外部系統權限以使用捷徑
  • 需要在實體裝置上進行測試以實現完整功能

原始 README

以下為關聯原始碼儲存庫的 README 原文快照,最新內容請造訪原始碼儲存庫。

flutter_assistant_intents

Expose app actions to voice assistants — iOS App Intents (Siri, Spotlight, the Shortcuts app) and Android app shortcuts — through one typed Dart handler API.

Two layers, mix freely:

  • Task preset (batteries included): onAddTask / onCompleteTask / onQueryTasks back ready-made Siri intents and launcher shortcuts for task/todo apps. Product-agnostic — iOS phrases use the application-name token, Android labels are passed in (and localizable) from Dart.
  • Generic action layer (any app domain): register onAction in Dart and route your own intents to it — custom iOS AppIntents with your own Siri phrases (one small Swift declaration each, template below) and fully Dart-defined custom Android shortcuts.

Why the Swift declaration? App Intents metadata is compiled statically — Siri discovers intent types and phrases at build time, so no plugin can create new intent types at runtime from Dart. This is an Apple platform constraint, not a library choice. The split this plugin makes is: intent type + phrases in Swift (a few lines, no logic), all fulfillment logic, parameters and results in Dart. On Android, custom shortcuts are 100% Dart-defined.

Support matrix

Capability iOS Android
Voice: "Add a task in <app>" (spoken title + due date + notes) ✅ App Intents, iOS 16+ ¹ ❌ (AppFunctions later, see below)
Voice: "Complete a task in <app>" ✅ ¹ ❌
Voice: "What are my tasks in <app>" (assistant speaks the list) ✅ ¹ ❌
Custom app-defined actions (any domain) ✅ custom intent in Runner → onAction ¹ ✅ custom shortcut → onAction
Shortcuts app / automations ✅ —
Siri phrase suggestions (AppShortcutsProvider) ✅ —
Confirmation prompts before a handler commits ✅ iOS 26+ (requestChoice) ❌
Launcher shortcuts (launch into the app) — ✅ ShortcutManagerCompat.pushDynamicShortcut
AppFunctions (assistant-invokable in-app functions) — 🚧 planned — androidx.appfunctions is alpha; a clearly marked stub (appfunctions/AppFunctionsIntegration.kt) documents the wiring and its host-app KSP requirement

¹ Works even from a cold start (app process not running) once the one-line headless-engine callback is configured — see Cold start. Without it, intents require the app process to be alive (foreground or suspended).

SiriKit custom intents are deliberately not used — App Intents is the modern replacement (SiriKit intents are deprecated as of iOS 26).

Quick start

dependencies:
  flutter_assistant_intents: ^1.0.0

Register handlers early in startup (they must be in place before an intent arrives) and refresh shortcut donations:

import 'package:flutter_assistant_intents/flutter_assistant_intents.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();

  AssistantIntents.instance.registerHandlers(
    AssistantIntentHandlers(
      onAddTask: (request) async {
        final task = await repository.create(request.title, due: request.dueDate);
        return AssistantTaskResult.success(
          message: "Added '${request.title}' to your list.",
          taskId: task.id,
        );
      },
      onCompleteTask: (request) async {
        final task = await repository.findByTitle(request.title);
        if (task == null) {
          return AssistantTaskResult.failure(
            "I couldn't find a task called '${request.title}'.",
          );
        }
        await repository.complete(task.id);
        return AssistantTaskResult.success(message: "Completed '${task.title}'.");
      },
      onQueryTasks: (request) async => request.filter == TaskQueryFilter.today
          ? repository.dueToday()
          : repository.allOpen(),
    ),
  );

  unawaited(AssistantIntents.instance.updateShortcuts());
  runApp(const MyApp());
}

All handlers are optional (register at least one). Rules of thumb:

  • Handlers should never throw — return AssistantTaskResult.failure('…') with a short, speakable sentence. Thrown errors are converted into a generic failure and reported via FlutterError.reportError.
  • When the user is logged out, return a friendly failure like "Please open the app and sign in first.".
  • result.message is spoken by Siri verbatim — keep it short and free of technical detail.

Generic actions — expose any app domain

Register a single onAction handler and dispatch on your own action ids:

AssistantIntents.instance.registerHandlers(
  AssistantIntentHandlers(
    onAction: (request) async => switch (request.action) {
      'order_coffee' => await _orderCoffee(request.parameters['size'] as String?),
      'clear_completed' => await _clearCompleted(),
      _ => const AssistantTaskResult.failure('Unknown action.'),
    },
  ),
);

Android — publish custom shortcuts entirely from Dart:

await AssistantIntents.instance.updateShortcuts(
  androidShortcuts: const AndroidShortcutsConfig(
    publishTaskShortcuts: false, // skip the task preset if you don't need it
    customShortcuts: [
      AndroidCustomShortcut(
        id: 'coffee',
        action: 'order_coffee',
        shortLabel: 'Coffee',
        longLabel: 'Order a coffee',
      ),
    ],
  ),
);

iOS — declare one small intent per action in your Runner target (this is where the Siri phrases live; the logic stays in Dart):

import AppIntents
import flutter_assistant_intents

@available(iOS 16.0, *)
struct OrderCoffeeIntent: AppIntent {
    static var title: LocalizedStringResource = "Order Coffee"
    static var openAppWhenRun: Bool = false

    @Parameter(title: "Size")
    var size: String?

    func perform() async throws -> some IntentResult & ProvidesDialog {
        let result = try await AssistantIntentBridge.shared.performAction(
            id: "order_coffee",
            parameters: ["size": size as Any]
        )
        return .result(dialog: IntentDialog(
            stringLiteral: result.message ?? (result.success ? "Done." : "Sorry, that failed.")
        ))
    }
}

@available(iOS 16.0, *)
struct MyAppShortcuts: AppShortcutsProvider {
    static var appShortcuts: [AppShortcut] {
        AppShortcut(
            intent: OrderCoffeeIntent(),
            phrases: ["Order a coffee in \(.applicationName)"],
            shortTitle: "Order Coffee",
            systemImageName: "cup.and.saucer"
        )
    }
}

See example/ios/Runner/AppDelegate.swift for a complete, working declaration.

Confirmation prompts (iOS 26+)

Before doing something destructive, a handler can ask instead of answering. Return AssistantTaskResult.needsConfirmation: the assistant speaks the question, offers the options, and calls the same handler again with the picked option in request.choice.

AssistantIntents.instance.registerHandlers(
  AssistantIntentHandlers(
    onAction: (request) async {
      if (request.action != 'clear_completed') {
        return const AssistantTaskResult.failure('Unknown action.');
      }

      // First pass: nothing chosen yet, so ask.
      if (request.choice == null) {
        return const AssistantTaskResult.needsConfirmation(
          dialog: 'Delete all completed tasks?',
          options: [
            AssistantChoice(
              id: 'delete',
              title: 'Delete them',
              style: AssistantChoiceStyle.destructive,
            ),
            AssistantChoice.cancel,
          ],
          fallbackMessage: 'Open the app to clear your completed tasks.',
        );
      }

      // Second pass: act on what the user picked.
      if (request.choice == 'cancel') {
        return const AssistantTaskResult.success(message: 'Nothing deleted.');
      }
      await repository.clearCompleted();
      return const AssistantTaskResult.success(message: 'All clear.');
    },
  ),
);

Nothing happens between the two calls: deciding what a choice means is the handler's job.

Where prompts are unavailable (iOS below 26, or an Android shortcut) the action stays unperformed and the assistant speaks fallbackMessage. That is the safe default for the destructive operations this is meant for, so write a fallback that tells the user where to finish the job.

A single intent run resolves at most 3 confirmation rounds, so a handler that keeps asking cannot trap the user in a loop.

Custom intents

Host apps that declare their own intents opt in with one conformance and one argument:

struct ClearCompletedIntent: AppIntent, AppIntentConfirming {
    static var title: LocalizedStringResource = "Clear Completed"

    func perform() async throws -> some IntentResult & ProvidesDialog {
        let payload = try await AssistantIntentBridge.shared.performAction(
            id: "clear_completed",
            confirming: self
        )
        return .result(dialog: IntentDialog(stringLiteral: payload.message ?? ""))
    }
}

Code generation — intents from YAML

Instead of hand-writing the Swift declarations, declare everything in assistant_intents.yaml and let the tool generate both sides (like intl / build_runner codegen):

dart run flutter_assistant_intents:generate --init   # writes a starter assistant_intents.yaml
dart run flutter_assistant_intents:generate          # (re)generates after every edit
# assistant_intents.yaml
task_preset: package   # package (plugin built-ins, iOS 17+) | wrappers (generated into Runner, iOS 16+) | none

actions:
  - id: order_coffee
    title: Order Coffee
    short_title: Coffee
    system_image: cup.and.saucer
    phrases:
      - "Order a coffee in ${app}"     # ${app} = the required app-name token
    parameters:
      - name: size
        type: string        # string | int | double | bool
        optional: true
        dialog: Which size?
    android_shortcut:
      short_label: Coffee
      long_label: Order a coffee

The generator writes:

  • ios/Runner/AssistantIntents.g.swift — one AppIntent per action (typed @Parameters calling your Dart onAction), an AppShortcutsProvider with your Siri phrases, and the task-preset intents (wrappers) or the AppIntentsPackage re-export (package). The file is registered in project.pbxproj automatically (idempotent; Xcode synchronized-group projects need no registration).
  • lib/assistant_intents.g.dart — an action-id constant per action and a ready generatedAndroidShortcuts config:
AssistantIntents.instance.registerHandlers(
  AssistantIntentHandlers(
    onAction: (request) async => switch (request.action) {
      orderCoffeeAction => await _orderCoffee(request.parameters),
      _ => const AssistantTaskResult.failure('Unknown action.'),
    },
  ),
);
await AssistantIntents.instance.updateShortcuts(
  androidShortcuts: generatedAndroidShortcuts,
);

task_preset: wrappers generates the AddTask/CompleteTask/QueryTasks intents directly into your Runner — use it when you need iOS 16.x or hit the framework metadata-extraction caveat. The config validates Apple's rules (app-name token in every phrase, max 10 App Shortcuts) at generation time.

Cleanup & safety rules:

  • Generated files are marked with a // GENERATED by … — DO NOT EDIT header and are fully rewritten on every run — actions removed from the YAML disappear from Swift, Siri phrases and Dart automatically; nothing piles up.
  • If you change the output: paths, the previous files are deleted and de-registered from project.pbxproj — but only files still carrying the GENERATED marker. A hand-written file sitting at an old output path is never touched (the tool prints a note and leaves it alone).
  • Never hand-edit the .g. files — edits are lost on the next run. Your own additional AppIntents belong in separate Swift files (e.g. in AppDelegate.swift or your own file); the generator neither overwrites nor deletes anything outside its two declared outputs.

iOS setup (host app)

The plugin requires iOS 16.0+ (set platform :ios, '16.0' in your Podfile and the Runner deployment target).

App Intents metadata is extracted from the app target at build time. Intents defined inside this plugin (a library) are only discovered when the host app re-exports them through an AppIntentsPackage (current SDKs mark the protocol iOS 17.0+). Add this once to your Runner target:

import AppIntents
import flutter_assistant_intents

@available(iOS 17.0, *)
struct RunnerAppIntentsPackage: AppIntentsPackage {
    static var includedPackages: [any AppIntentsPackage.Type] {
        [FlutterAssistantIntentsPackage.self]
    }
}

iOS 16.x caveat: the AppIntentsPackage re-export requires iOS 17. To surface the built-in intents on iOS 16 devices, declare thin wrapper intents in the Runner target that call the plugin's public AssistantIntentBridge — the wrappers live in the app target, so metadata extraction sees them on any iOS 16 device.

use_frameworks! / release-build caveat: metadata extraction from intents living in an embedded framework has known gaps in some release/TestFlight configurations. If the built-in intents don't appear in the Shortcuts app of a release build, declare thin wrapper intents in the Runner (same pattern as above) — wrappers in the app target are always extracted. Custom intents you declare via the generic action layer live in the Runner already and are unaffected.

Cold start

Intents run in the app process (openAppWhenRun = false) and are fulfilled by your Dart handlers, so the Flutter engine must be running. When Siri runs an intent while the process is cold, the plugin can boot a headless Flutter engine — enable it with one call in application(_:didFinishLaunchingWithOptions:):

FlutterAssistantIntentsPlugin.setPluginRegistrantCallback { engine in
    GeneratedPluginRegistrant.register(with: engine)
}

By default the headless engine runs your main(). If your main() does work that is unsafe without UI (or you want a faster boot), point it at a dedicated entrypoint that only registers the handlers:

FlutterAssistantIntentsPlugin.setPluginRegistrantCallback(entrypoint: "assistantMain") { engine in
    GeneratedPluginRegistrant.register(with: engine)
}
@pragma('vm:entry-point')
void assistantMain() {
  WidgetsFlutterBinding.ensureInitialized();
  registerMyAssistantHandlers(); // same registration main() uses
}

Without the callback, cold-start intents answer "Please open the app first, then try again." (the plugin waits up to 5 seconds for registerHandlers and a further 10 seconds for each handler reply — it never hangs Siri). Either way: register handlers as early as possible so warm/background launches always succeed.

Android setup (host app)

No manifest changes are required for dynamic shortcuts. Call updateShortcuts() on startup (pass localized labels):

await AssistantIntents.instance.updateShortcuts(
  androidShortcuts: const AndroidShortcutsConfig(
    addTaskLabel: 'Add task',
    queryTodayLabel: 'Today',
  ),
);

Notes:

  • Shortcuts are published with pushDynamicShortcut (additive) — the plugin never touches shortcuts your app publishes itself.
  • Keep short labels under ~10 characters; launchers truncate longer ones.
  • Labels are supplied from Dart, so re-call updateShortcuts() after a locale change to re-publish translated labels.
  • To surface the shortcuts on Google surfaces (Assistant/Gemini "shortcuts" suggestions), add androidx.core:core-google-shortcuts to your app — donation then happens automatically for pushed shortcuts. The plugin does not impose that dependency.

Tapping a shortcut launches (or resumes) the app with an intent extra that the plugin converts into the same Dart handler calls:

  • Add task → onAddTask with an empty title (shortcuts cannot carry free-form text) — treat it as "open the add-task flow".
  • Today → onQueryTasks with TaskQueryFilter.today — navigate to your today view.
  • Custom shortcut → onAction with the shortcut's action id.

If your MainActivity overrides onNewIntent, keep calling super.onNewIntent(intent) so plugin listeners receive it (the default FlutterActivity already does).

API surface

Symbol Purpose
AssistantIntents.instance Entry point (singleton)
registerHandlers(AssistantIntentHandlers) Wire your app's logic (all handlers optional, min. one)
updateShortcuts({AndroidShortcutsConfig}) Refresh platform shortcut donations (no-op-safe everywhere)
AddTaskRequest title, dueDate?, notes?
CompleteTaskRequest title (resolve fuzzily in your app)
QueryTasksRequest filter (TaskQueryFilter.today / .all)
AssistantActionRequest action id + parameters map (generic layer)
AssistantTask id, title, dueDate?, isCompleted
AssistantTaskResult .success(message:, taskId:) / .failure(message) / .needsConfirmation(dialog:, options:)
AssistantChoice / AssistantChoiceStyle One option of a confirmation prompt (normal, destructive, cancel)
AndroidShortcutsConfig / AndroidCustomShortcut Android shortcut labels + custom shortcuts

Example

See example/ for a runnable app using both the task preset and the generic action layer.

License

MIT

所屬分類

依賴

依賴清單 1 項

原始碼儲存庫

使用此套件的專案

暫無相關資料。

所在榜單