搜索插件、代码仓与 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 项

源码仓库

使用此插件的项目

暂无相关数据。

所在榜单