패키지, 저장소 및 앱 카테고리 검색

패키지 상세v1.5.0

flutter_assistant_intents

타입화된 Dart 핸들러를 사용하여 음성 보조 기능(iOS App Intents(Siri, Spotlight, Shortcuts), Android 앱 단축키)에 task-app 작업을 노출합니다.

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 스팟라이트 및 안드로이드 빠른 설정에서 맥락 인식 단축키 만들기
  • 앱 단축키를 통한 작업 자동화 지원
  • 자연어 명령을 사용하여 사용자 워크플로우와 통합
  • 시스템 수준 인터페이스에서 원활한 딥 링크 허용
  • 크로스 플랫폼 어시스턴트 호환 기능 구축

주요 기능

  • 앱 인텐트용 타입 지정된 다트 핸들러
  • iOS 및 안드로이드용 크로스 플랫폼 지원
  • 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 개

소스 저장소

이 패키지를 사용하는 프로젝트

아직 데이터가 없습니다.

순위