Note: This tutorial guides you through a simplified version of the counter example. For a more advanced implementation with multiple intents and parameter handling, please refer to the
example/counterdirectory in this project.
This tutorial will guide you through creating a simple Flutter counter app that can be controlled by Siri voice commands using the flutter_app_intents plugin.
flutter create counter_intents_tutorial
cd counter_intents_tutorial
Edit pubspec.yaml and add the dependency:
dependencies:
flutter:
sdk: flutter
flutter_app_intents: ^0.7.0 # Use the latest version
cupertino_icons: ^1.0.8
Then run:
flutter pub get
The App Intents framework requires iOS 16.0+. Update the deployment target:
ios/Podfile and ensure the platform is set to iOS 16.0:platform :ios, '16.0'
ios/Runner.xcodeproj/project.pbxproj and update the deployment target:
IPHONEOS_DEPLOYMENT_TARGET11.0 (or whatever version) to 16.0Replace the contents of lib/main.dart:
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_app_intents/flutter_app_intents.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Counter App Intents Tutorial',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
home: const CounterHomePage(),
);
}
}
class CounterHomePage extends StatefulWidget {
const CounterHomePage({super.key});
@override
State<CounterHomePage> createState() => _CounterHomePageState();
}
class _CounterHomePageState extends State<CounterHomePage> {
final FlutterAppIntentsClient _client = FlutterAppIntentsClient.instance;
int _counter = 0;
String _status = 'Initializing...';
@override
void initState() {
super.initState();
_setupAppIntents();
}
Future<void> _setupAppIntents() async {
if (!Platform.isIOS) {
setState(() {
_status = 'App Intents are only supported on iOS';
});
return;
}
try {
// Create the increment counter intent
final incrementIntent = AppIntentBuilder()
.identifier('increment_counter')
.title('Increment Counter')
.description('Increment the counter by one')
.build();
// Register the intent with its handler
await _client.registerIntents({
incrementIntent: _handleIncrementIntent,
});
await _client.updateShortcuts();
setState(() {
_status = 'App Intent registered successfully!\nTry saying: "Hey Siri, increment counter with counter intents tutorial"';
});
} catch (e) {
setState(() {
_status = 'Error: $e';
});
}
}
Future<AppIntentResult> _handleIncrementIntent(
Map<String, dynamic> parameters,
) async {
setState(() {
_counter++;
});
// Donate the intent to help Siri learn user patterns
await _client.donateIntent('increment_counter', parameters);
return AppIntentResult.successful(
value: 'Counter incremented to $_counter',
needsToContinueInApp: true,
);
}
void _incrementCounter() {
_handleIncrementIntent({});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('Counter App Intents Tutorial'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Card(
color: Colors.blue.shade50,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
const Text(
'App Intents Status:',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
_status,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 12),
),
],
),
),
),
const SizedBox(height: 40),
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 20),
const Card(
color: Colors.green,
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(
'Try saying:\n"Hey Siri, increment counter with counter intents tutorial"',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
Note: The status message and the UI card in the example above include a suggested Siri command with the app name “counter intents tutorial”. When you test with Siri, make sure to use your app’s actual display name, which might be different.
For Siri to discover our intents, we need to declare them statically in Swift. We’ll separate our App Intents logic from the AppDelegate to keep the code organized.
First, ensure your ios/Runner/AppDelegate.swift file is clean and only contains the standard Flutter setup. Replace its contents with the following:
import Flutter
import UIKit
@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)
}
}
Next, create a new file named ios/Runner/AppShortcuts.swift. This file will contain the App Intent definition and the App Shortcuts provider.
import AppIntents
import flutter_app_intents
// Simple error for App Intents
enum AppIntentError: Error {
case executionFailed(String)
}
// App Intent that bridges to Flutter plugin
@available(iOS 16.0, *)
struct CounterIntent: AppIntent {
static var title: LocalizedStringResource = "Increment Counter"
static var description = IntentDescription("Increment the counter by one")
static var isDiscoverable = true
static var openAppWhenRun: Bool = 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)
}
}
}
@available(iOS 16.0, *)
struct CounterAppShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: CounterIntent(),
phrases: [
"Increment counter with \(.applicationName)",
"Add one with \(.applicationName)",
"Count up using \(.applicationName)"
],
shortTitle: "Increment",
systemImageName: "plus.circle"
)
}
}
Let’s break down each part of the AppShortcuts.swift file:
import AppIntents
import flutter_app_intents
enum AppIntentError: Error {
case executionFailed(String)
}
import AppIntents: iOS 16+ framework for Siri integrationimport flutter_app_intents: The Flutter plugin’s native iOS moduleAppIntentError: Custom error type for handling intent failuresCounterIntent)@available(iOS 16.0, *)
struct CounterIntent: AppIntent {
static var title: LocalizedStringResource = "Increment Counter"
static var description = IntentDescription("Increment the counter by one")
static var isDiscoverable = true
func perform() async throws -> some IntentResult & ReturnsValue<String> {
// ...
}
}
@available(iOS 16.0, *): Ensures this only runs on iOS 16+static var title: What Siri will say/display to usersstatic var description: Detailed description for the Shortcuts appstatic var isDiscoverable: Makes the intent visible in Shortcuts appReturnsValue<String>: Tells iOS this intent returns a text responseThe perform() function is where the magic happens:
FlutterAppIntentsPlugin.sharedhandleIntentInvocation with the identifier "increment_counter"CounterAppShortcuts)@available(iOS 16.0, *)
struct CounterAppShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
// ...
}
}
This struct tells iOS about your shortcuts:
AppShortcut: Defines a shortcut that appears in the Shortcuts appintent: Links to the CounterIntent we defined abovephrases: The exact words users can say to Siri
\(.applicationName): Automatically replaced with your app’s nameshortTitle: Short name shown in Shortcuts appsystemImageName: iOS system icon to displayCounterAppShortcuts.CounterIntent.perform().FlutterAppIntentsPlugin.shared.handleIntentInvocation().main.dart.flutter clean
flutter pub get
cd ios && pod install && cd ..
flutter run
Make sure the app is installed on your device (not just running in debug mode).
Open the Shortcuts app on your iOS device. You should see “Counter Intents Tutorial” in the “App Shortcuts” section.
AppShortcuts definition.Now that you have a basic working example, you can:
Congratulations! You now have a working Flutter app with Siri integration using App Intents.