flutter_app_intents

Flutter App Intents

Flutter App Intents Logo

A Flutter plugin for integrating Apple App Intents with your iOS applications. This plugin enables your Flutter app to work seamlessly with Siri, Shortcuts, Spotlight, and other system experiences on iOS 16.0 and later.

Features

Documentation

📖 Complete Documentation - Visit our comprehensive documentation website with tutorials, examples, and API reference.

Requirements

Installation

Add this to your package’s pubspec.yaml file:

dependencies:
  flutter_app_intents: ^0.7.0

Swift Package Manager (Advanced)

For iOS developers who want to use the native Swift components directly, this package also supports Swift Package Manager:

// In Package.swift
dependencies: [
    .package(url: "https://github.com/cbonello/flutter_app_intents", from: "0.7.0")
]

Or add via Xcode: File → Add Package Dependencieshttps://github.com/cbonello/flutter_app_intents

Note: SPM support is provided for advanced use cases. Most Flutter developers should use the standard plugin installation above. See SPM_README.md for detailed SPM integration instructions.

Architecture Overview

This plugin uses a hybrid approach combining:

  1. Static Swift intents in your main iOS app target (required for iOS discovery)
  2. Dynamic Flutter handlers registered through the plugin (your business logic)
iOS Shortcuts/Siri → Static Swift Intent → Flutter Plugin Bridge → Your Flutter Handler

The static Swift intents act as a bridge, calling your Flutter handlers when executed.

Simple Example

Note: This example shows the core logic, but a complete implementation requires a corresponding static intent in your iOS app’s AppDelegate.swift. See the iOS Configuration section for details.

Create a voice-controlled counter app in just a few steps:

// 1. Register your intent
final client = FlutterAppIntentsClient.instance;
final intent = AppIntentBuilder()
    .identifier('increment_counter')
    .title('Increment Counter')
    .build();

await client.registerIntent(intent, (parameters) async {
  // Your business logic here
  incrementCounter();
  return AppIntentResult.successful(value: 'Counter incremented!');
});
// 2. Add static intent to iOS (AppDelegate.swift)
import AppIntents

struct IncrementCounterIntent: AppIntent {
    static var title: LocalizedStringResource = "Increment Counter"

    func perform() async throws -> some IntentResult {
        await FlutterAppIntentsPlugin.shared.handleIntent("increment_counter", [:])
        return .result()
    }
}

Result: Say “Hey Siri, increment counter” and your Flutter function runs! 🎉

Quick Start

📖 New to App Intents? Check out our Step-by-Step Tutorial for a complete walkthrough from flutter create to working Siri integration!

1. Import the package

import 'package:flutter_app_intents/flutter_app_intents.dart';

2. Add static intents to iOS (Required)

⚠️ First, add static App Intents to your iOS AppDelegate.swift (see iOS Configuration section below)

3. Create and register Flutter handlers

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Register intents during app initialization
    _setupAppIntents();
    
    return MaterialApp(
      title: 'My App',
      home: MyHomePage(),
    );
  }
  
  Future<void> _setupAppIntents() async {
    final client = FlutterAppIntentsClient.instance;
    
    // Create an intent using the builder
    final incrementIntent = AppIntentBuilder()
        .identifier('increment_counter')
        .title('Increment Counter')
        .description('Increments the counter by one')
        .parameter(const AppIntentParameter(
          name: 'amount',
          title: 'Amount',
          type: AppIntentParameterType.integer,
          isOptional: true,
          defaultValue: 1,
        ))
        .build();
    
    // Register with a handler
    await client.registerIntent(incrementIntent, (parameters) async {
      final amount = parameters['amount'] as int? ?? 1;
      
      // Your business logic here
      incrementCounter(amount);
      
      return AppIntentResult.successful(
        value: 'Counter incremented by $amount',
      );
    });
  }
}

3. Handle intent execution

Future<AppIntentResult> handleIncrementIntent(Map<String, dynamic> parameters) async {
  try {
    final amount = parameters['amount'] as int? ?? 1;
    
    // Perform your app's logic
    final newValue = incrementCounter(amount);
    
    // Donate the intent to help Siri learn
    await FlutterAppIntentsClient.instance.donateIntent(
      'increment_counter',
      parameters,
    );
    
    return AppIntentResult.successful(
      value: 'Counter is now $newValue',
    );
  } catch (e) {
    return AppIntentResult.failed(
      error: 'Failed to increment counter: $e',
    );
  }
}

Our plugin excels at handling app navigation through voice commands and shortcuts. Here’s how to implement navigation intents:

For navigation, use needsToContinueInApp: true to tell iOS to focus your app and OpensIntent return type in Swift:

iOS Implementation:

@available(iOS 16.0, *)
struct OpenProfileIntent: AppIntent {
    static var title: LocalizedStringResource = "Open Profile"
    static var description = IntentDescription("Open user profile page")
    static var isDiscoverable = true
    static var openAppWhenRun = true
    
    @Parameter(title: "User ID")
    var userId: String?
    
    func perform() async throws -> some IntentResult & ReturnsValue<String> & OpensIntent {
        let plugin = FlutterAppIntentsPlugin.shared
        let result = await plugin.handleIntentInvocation(
            identifier: "open_profile",
            parameters: ["userId": userId ?? "current"]
        )
        
        if let success = result["success"] as? Bool, success {
            let value = result["value"] as? String ?? "Profile opened"
            return .result(value: value) // This opens/focuses the app
        } else {
            let errorMessage = result["error"] as? String ?? "Failed to open profile"
            throw AppIntentError.executionFailed(errorMessage)
        }
    }
}

Flutter Handler:

Future<AppIntentResult> _handleOpenProfileIntent(
  Map<String, dynamic> parameters,
) async {
  final userId = parameters['userId'] as String? ?? 'current';
  
  // Navigate to the target page
  Navigator.of(context).pushNamed('/profile', arguments: {'userId': userId});
  
  return AppIntentResult.successful(
    value: 'Opening profile for user $userId',
    needsToContinueInApp: true, // Critical: focuses the app
  );
}

Common Navigation Patterns

1. Deep Linking with Parameters

// Navigate to specific content with parameters
Future<AppIntentResult> _handleOpenChatIntent(Map<String, dynamic> parameters) async {
  final contactName = parameters['contactName'] as String;
  
  Navigator.of(context).pushNamed('/chat', arguments: {
    'contactName': contactName,
    'openedViaIntent': true,
  });
  
  return AppIntentResult.successful(
    value: 'Opening chat with $contactName',
    needsToContinueInApp: true,
  );
}

2. Search Navigation

// Handle search queries with navigation
Future<AppIntentResult> _handleSearchIntent(Map<String, dynamic> parameters) async {
  final query = parameters['query'] as String;
  
  Navigator.of(context).pushNamed('/search', arguments: {'query': query});
  
  return AppIntentResult.successful(
    value: 'Searching for "$query"',
    needsToContinueInApp: true,
  );
}

3. Settings/Configuration Navigation

// Navigate to specific settings pages
Future<AppIntentResult> _handleOpenSettingsIntent(Map<String, dynamic> parameters) async {
  final section = parameters['section'] as String? ?? 'general';
  
  Navigator.of(context).pushNamed('/settings/$section');
  
  return AppIntentResult.successful(
    value: 'Opening $section settings',
    needsToContinueInApp: true,
  );
}

If you’re using GoRouter, the pattern is similar:

Future<AppIntentResult> _handleNavigationIntent(Map<String, dynamic> parameters) async {
  final route = parameters['route'] as String;
  
  // Use GoRouter for navigation
  context.go(route);
  
  return AppIntentResult.successful(
    value: 'Navigating to $route',
    needsToContinueInApp: true,
  );
}

AppShortcuts for Navigation

Add navigation shortcuts to your AppShortcutsProvider:

@available(iOS 16.0, *)
struct AppShortcuts: AppShortcutsProvider {
    static var appShortcuts: [AppShortcut] {
        return [
            // Navigation shortcuts
            AppShortcut(
                intent: OpenProfileIntent(),
                phrases: [
                    "Open my profile in ${applicationName}",
                    "Show profile using ${applicationName}",
                    "Go to profile with ${applicationName}"
                ]
            ),
            AppShortcut(
                intent: OpenChatIntent(),
                phrases: [
                    "Chat with \\(.contactName) using ${applicationName}",
                    "Open chat with \\(.contactName) in ${applicationName}",
                    "Message \\(.contactName) with ${applicationName}"
                ]
            )
        ]
    }
}
Intent Type Return Type Use Case Example
Query ReturnsValue<String> Get information only “Get counter value”, “Check weather”
Action + App Opening ReturnsValue<String> & OpensIntent Execute + show result “Increment counter”, “Send message”
Navigation ReturnsValue<String> & OpensIntent Navigate to pages “Open profile”, “Show chat”

API Reference

Flutter App IntentsClient

The main client class for managing App Intents:

Methods

AppIntent

Represents an App Intent configuration:

const AppIntent({
  required String identifier,      // Unique ID
  required String title,          // Display name
  required String description,    // What it does
  List<AppIntentParameter> parameters = const [],
  bool isEligibleForSearch = true,
  bool isEligibleForPrediction = true,
  AuthenticationPolicy authenticationPolicy = AuthenticationPolicy.none,
});

AppIntentParameter

Defines parameters that can be passed to intents:

const AppIntentParameter({
  required String name,           // Parameter name
  required String title,          // Display title
  required AppIntentParameterType type,
  String? description,
  bool isOptional = false,
  dynamic defaultValue,
});

AppIntentResult

Result returned from intent execution:

// Successful result
AppIntentResult.successful(
  value: 'Operation completed',
  needsToContinueInApp: false,
);

// Failed result
AppIntentResult.failed(
  error: 'Something went wrong',
);

AppIntentBuilder

Fluent API for creating intents:

final intent = AppIntentBuilder()
    .identifier('my_intent')
    .title('My Intent')
    .description('Does something useful')
    .parameter(myParameter)
    .eligibleForSearch(true)
    .authenticationPolicy(AuthenticationPolicy.requiresAuthentication)
    .build();

Enhanced Intent Donation

The plugin provides advanced intent donation capabilities to help Siri learn user patterns and provide better predictions.

Basic Intent Donation

// Donate intent for Siri learning
await FlutterAppIntentsClient.instance.donateIntent(
  'my_intent',
  {'param': 'value'},
);

Donation Best Practices

Donate intents after successful execution to help Siri learn user patterns:

// Execute the intent action
final result = await performAction();

// Donate if successful
if (result.isSuccess) {
  await FlutterAppIntentsClient.instance.donateIntent(
    'my_intent',
    parameters,
  );
}

When to donate:

When not to donate:

iOS Configuration

Required Setup: Static App Intents in Main App Target

⚠️ Important: iOS App Intents framework requires static intent declarations in your main app target, not just dynamic registration from the plugin.

Add this code to your iOS app’s AppDelegate.swift:

import Flutter
import UIKit
import AppIntents
import flutter_app_intents

@main
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

// Static App Intents that bridge to Flutter handlers
@available(iOS 16.0, *)
struct MyCounterIntent: AppIntent {
    static var title: LocalizedStringResource = "Increment Counter"
    static var description = IntentDescription("Increment the counter by one")
    static var isDiscoverable = true
    static var openAppWhenRun = true
    
    func perform() async throws -> some IntentResult & ReturnsValue<String> & OpensIntent {
        let plugin = FlutterAppIntentsPlugin.shared
        let result = await plugin.handleIntentInvocation(
            identifier: "increment_counter", 
            parameters: [:]
        )
        
        if let success = result["success"] as? Bool, success {
            let value = result["value"] as? String ?? "Counter incremented"
            return .result(value: value)
        } else {
            let errorMessage = result["error"] as? String ?? "Failed to increment counter"
            throw AppIntentError.executionFailed(errorMessage)
        }
    }
}

// Error handling for App Intents
enum AppIntentError: Error {
    case executionFailed(String)
}

// AppShortcutsProvider for Siri/Shortcuts discovery
@available(iOS 16.0, *)
struct AppShortcuts: AppShortcutsProvider {
    static var appShortcuts: [AppShortcut] {
        return [
            AppShortcut(
                intent: MyCounterIntent(),
                phrases: [
                    "Increment counter with \(.applicationName)",
                    "Add one with \(.applicationName)",
                    "Count up using \(.applicationName)"
                ]
            )
        ]
    }
}

App Shortcuts Phrase Best Practices

When defining phrases for your App Shortcuts, follow these best practices for optimal user experience and Siri recognition:

⚠️ Important Limitation: Static Phrases Only

App Shortcuts phrases CANNOT be defined dynamically. This is a fundamental limitation of Apple’s App Intents framework:

// ❌ DOES NOT WORK - phrases must be static literals
phrases: [
    "\(userDefinedPhrase) with \(.applicationName)",     // Won't compile
    dynamicPhraseVariable,                               // Won't compile
    generatePhrase()                                     // Won't compile
]

// ✅ WORKS - static phrases with dynamic parameters
phrases: [
    "Send message to \(.contactName) with \(.applicationName)",    // ✅ Parameter is dynamic
    "Set timer for \(.duration) using \(.applicationName)",       // ✅ Parameter is dynamic
    "Play \(.songName) in \(.applicationName)"                    // ✅ Parameter is dynamic
]

Why phrases must be static:

Workarounds for dynamic content:

  1. Use parameters for the dynamic parts (user names, amounts, etc.)
  2. Provide comprehensive variations to cover common use cases
  3. Create multiple intent types for different scenarios instead of one dynamic intent

📊 Phrase Quantity Limits and Guidelines

While Apple doesn’t publish exact hard limits, there are practical constraints on the number of phrases:

Recommended Limits:

// ✅ GOOD - Focused, natural variations (4 phrases)
AppShortcut(
    intent: SendMessageIntent(),
    phrases: [
        "Send message to \(.contactName) with \(.applicationName)",
        "Text \(.contactName) using \(.applicationName)",
        "Message \(.contactName) in \(.applicationName)",
        "Write to \(.contactName) with \(.applicationName)"
    ]
)

// ❌ EXCESSIVE - Too many similar phrases (impacts performance)
AppShortcut(
    intent: SendMessageIntent(),
    phrases: [
        "Send message to \(.contactName) with \(.applicationName)",
        "Send a message to \(.contactName) with \(.applicationName)",
        "Send text message to \(.contactName) with \(.applicationName)",
        "Send a text message to \(.contactName) with \(.applicationName)",
        // ... 15+ more variations
    ]
)

Performance Impact:

Best Strategy:

  1. Start with 3-4 core phrases that feel most natural
  2. Test with real users to see which phrases they actually use
  3. Add variations based on user feedback rather than guessing
  4. Remove unused phrases to optimize performance

1. Include App Name for Disambiguation

✅ Recommended:

phrases: [
    "Increment counter with \(.applicationName)",
    "Add one using \(.applicationName)",
    "Count up in \(.applicationName)"
]

❌ Avoid:

phrases: [
    "Increment counter",  // Too generic, conflicts with other apps
    "Add one"             // Ambiguous without context
]

2. Use Natural Prepositions

Choose prepositions that sound natural in conversation:

3. Provide Multiple Variations

Offer 3-5 phrase variations to accommodate different user preferences:

phrases: [
    "Increment counter with \(.applicationName)",      // Formal
    "Add one using \(.applicationName)",               // Casual
    "Count up in \(.applicationName)",                 // Alternative verb
    "Bump counter with \(.applicationName)",           // Colloquial
    "Increase count using \(.applicationName)"         // Descriptive
]

4. Keep Phrases Concise but Descriptive

5. Alternative Patterns

App Name at Beginning (less common but valid):

phrases: [
    "Use \(.applicationName) to increment counter",
    "Tell \(.applicationName) to reset timer"
]

Action-First Pattern (most natural):

phrases: [
    "Start workout with \(.applicationName)",
    "Send message using \(.applicationName)",
    "Check weather in \(.applicationName)"
]

6. Testing Your Phrases

7. Common Phrase Patterns by Intent Type

Action Intents:

"[Action] [Object] with \(.applicationName)"
"[Verb] [Noun] using \(.applicationName)"

Query Intents:

"Get [Data] from \(.applicationName)"
"Check [Status] in \(.applicationName)"
"What's [Information] using \(.applicationName)"

Navigation Intents:

"Open [Page] in \(.applicationName)"
"Go to [Section] using \(.applicationName)"
"Show [Content] with \(.applicationName)"

Info.plist Configuration

Add these permissions and configuration to your iOS Info.plist:

<key>NSMicrophoneUsageDescription</key>
<string>This app uses microphone for Siri integration</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>This app uses speech recognition for Siri integration</string>

<!-- App Intents Configuration -->
<key>NSAppIntentsConfiguration</key>
<dict>
    <key>NSAppIntentsPackage</key>
    <string>your_app_bundle_id</string>
</dict>
<key>NSAppIntentsMetadata</key>
<dict>
    <key>NSAppIntentsSupported</key>
    <true/>
</dict>

Minimum Deployment Target

Ensure your iOS deployment target is set to 16.0 or later:

# ios/Podfile
platform :ios, '16.0'

Parameter Types

The following parameter types are supported:

Authentication Policies

Control when intents can be executed:

Best Practices

General Practices

  1. Keep intent names simple and descriptive
  2. Use appropriate parameter types
  3. Provide good descriptions for discoverability
  4. Handle errors gracefully
  5. Test with Siri and Shortcuts app

Long-Running Operations and Loading States

  1. No loading indicators during intent execution - App Intents run outside your Flutter app’s UI context through iOS’s system-level framework, so you cannot display loading indicators during execution
  2. Use needsToContinueInApp: true for long operations - Return immediately and continue processing in your app:
    Future<AppIntentResult> _handleLongOperation(Map<String, dynamic> parameters) async {
      // Start background work but return immediately
      _startBackgroundWork();
         
      return AppIntentResult.successful(
        value: 'Operation started, opening app for progress...',
        needsToContinueInApp: true,  // Opens your app where you can show progress
      );
    }
    
  3. Show progress in Flutter app after intent redirect - Display loading indicators in your Flutter UI after the intent opens your app
  4. Consider timeout handling - Long-running intents may timeout at the system level, so break work into smaller chunks

App Opening Behavior

  1. Use static var openAppWhenRun = true in Swift intents that should open the app
  2. Add & OpensIntent to the return type for intents that open the app
  3. Include needsToContinueInApp: true in Flutter results for visual feedback
  4. Choose appropriate behavior: Some intents (like queries) may not need to open the app
  1. Always use needsToContinueInApp: true for navigation intents
  2. Add static var openAppWhenRun = true to force app opening
  3. Use ReturnsValue<String> & OpensIntent return type in Swift
  4. Handle app state properly - check if context is still mounted
  5. Pass meaningful parameters to destination pages
  6. Consider app lifecycle - navigation may happen when app is backgrounded

Intent Donation Strategy

  1. Donate intents strategically:
    • Use enhanced donation with metadata for better Siri learning
    • Donate after successful execution only
    • Use appropriate relevance scores based on usage patterns
    • Provide contextual information to improve predictions
    • Use batch donations for related intents
  2. Navigation intents should have high relevance (0.8-1.0) when user-initiated
  3. Monitor donation performance and adjust relevance scores based on user behavior

App Integration

  1. Static intents must match Flutter handlers - ensure identifier consistency
  2. Handle app cold starts - navigation intents may launch your app
  3. Test edge cases - what happens when target pages don’t exist?
  4. Provide fallback navigation - graceful handling of invalid routes

Examples

📚 Tutorial: Simple Counter App

Our Step-by-Step Tutorial walks you through building a complete counter app with Siri integration from scratch.

🔍 Example Apps

Check out the example apps for complete implementations showing different App Intent patterns:

1. Counter Example - Action Intents

2. Navigation Example - Navigation Intents

3. Weather Example - Query Intents

Advanced Features

Troubleshooting

“App Intents are only supported on iOS”

This plugin only works on iOS 16.0+. Make sure you’re testing on a compatible device or simulator.

Intents not appearing in Siri/Shortcuts

Most Common Issues: Missing static App Intents or disabled Siri integration

  1. Verify static intents are declared in your AppDelegate.swift (see iOS Configuration above)
  2. Ensure AppShortcutsProvider exists in your main app target
  3. Enable Siri for App Shortcuts: In iOS Shortcuts app → [Your App] Shortcuts → Toggle ON the Siri switch (it’s OFF by default)
  4. Check intent identifiers match between static Swift intents and Flutter handlers
  5. Restart the app completely after adding static intents
  6. Ensure intents are registered successfully on Flutter side
  7. Check that isEligibleForPrediction is true
  8. Try donating the intent after manual execution
  9. Restart the Shortcuts app

Architecture Note: iOS App Intents framework requires static intent declarations at compile time for Siri/Shortcuts discovery. Dynamic registration from Flutter plugins alone is not sufficient.

Voice commands not recognized

  1. Use simple, clear command phrases
  2. Test different phrasings
  3. Check Siri’s language settings
  4. Verify intent titles are descriptive
  1. Verify needsToContinueInApp: true in Flutter result
  2. Check OpensIntent return type in Swift intent
  3. Ensure routes exist in your app’s navigation setup
  4. Test app lifecycle - try when app is backgrounded vs foreground
  5. Check mounted context before navigation calls
  6. Verify parameter passing to destination screens

Intent donations not improving predictions

  1. Ensure proper relevance scores: Use higher scores (0.8-1.0) for frequently used actions
  2. Provide meaningful context: Include feature names, user actions, and usage patterns
  3. Donate consistently: Only donate after successful intent execution
  4. Use batch donations: Group related intents for better learning
  5. Monitor and adjust: Regularly review and update relevance scores based on usage analytics

Intent Donation Errors

If intent donation fails, check:

Apple Documentation References

For deeper understanding of the underlying iOS concepts, refer to these official Apple resources:

Core App Intents Framework

Siri Integration

Parameters and Data Types

Intent Donation and Learning

Authentication and Security

Advanced Topics

WWDC Sessions

Design Guidelines

Contributing

This package is an independent Flutter plugin for Apple App Intents integration.

License

This project is licensed under the MIT License - see the LICENSE file for details.