SDK
Flutter SDK
Add deep links, deferred deep links, install attribution and in-app events to a Flutter app with the appy_sdk plugin for iOS and Android.
The appy_sdk plugin is a thin Dart layer over the native iOS SDK and Android SDK. Deep links and attribution arrive as Dart streams with typed results, and everything else is one method call away. The native guides go deeper on each topic; this page covers what is specific to Flutter.
Requirements
- Flutter 3.44 or later and Dart 3.12 or later.
- iOS 13 or later, Android 5.0 (API 21) or later.
- An app registered with Appy on the Enterprise plan, and its publishable key (
appy_pk_…).
Install the plugin
flutter pub add appy_sdk| Platform | Native SDK |
|---|---|
| iOS with Swift Package Manager (the default since Flutter 3.44) | Added automatically from the AppySDK Swift package. |
| iOS with CocoaPods | Add pod 'AppySDK', :git => 'https://github.com/appy-to/appy-ios-sdk.git', :tag => '1.0.0' inside target 'Runner' do in ios/Podfile. The CocoaPods trunk no longer accepts new pods, so the pod comes from its Git repository. |
| Android | Added automatically from Maven Central as to.appy:appy-sdk. |
Register your app
Register the app once under Apps & SDK in the dashboard or with POST /v1/apps: the Apple Team ID and bundle ID for iOS, the package name and every signing certificate’s SHA-256 fingerprint for Android. You get the publishable key and your link domain, such as acme.appy.to. See Register your app in the iOS guide and in the Android guide.
Set up iOS and Android
Appy links reach your Dart code through Appy.onDeepLink. Flutter’s built-in deep linking would also push the link’s path, such as /spring, onto your router, so turn it off on both platforms.
- 1
iOS: Associated Domains
In Xcode, open
ios/Runner.xcworkspace, add the Associated Domains capability to the Runner target and enterapplinks:acme.appy.to. While developing, addapplinks:acme.appy.to?mode=developertoo. - 2
iOS: URL scheme
Under Info > URL Types, add your scheme, for example
acme. In-app browsers that ignore Universal Links open it instead. - 3
iOS: Flutter deep linking
In
ios/Runner/Info.plist, setFlutterDeepLinkingEnabledtoNO. - 4
Android: intent filters
In
android/app/src/main/AndroidManifest.xml, add the App Link and custom-scheme filters below toMainActivity, together withflutter_deeplinking_enabledset tofalse. Keep Flutter’s defaultsingleToplaunch mode. - 5
Android: backups
Exclude
to.appy.sdk.xmlfrom Auto Backup as shown in Keep the first launch a first launch, so a reinstall still gets its first launch.
<meta-data android:name="flutter_deeplinking_enabled" android:value="false" />
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="acme.appy.to" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="acme" />
</intent-filter>The plugin listens to both the app delegate and the scene delegate on iOS, and to the Flutter activity on Android, so no native code is needed in your app.
Configure the plugin
import 'package:appy_sdk/appy_sdk.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Appy.configure(AppyConfiguration(
publishableKey: 'appy_pk_live_…',
linkDomains: const ['acme.appy.to'],
logLevel: kDebugMode ? AppyLogLevel.debug : AppyLogLevel.error,
));
runApp(const MyApp());
}| Option | Default | Meaning |
|---|---|---|
publishableKey | required | Your appy_pk_… key. |
linkDomains | [] | Hosts of your Appy links. The first one is used by buildLink. |
emailLinkDomains | [] | Click-tracking domains of your email tool that open the app. The native SDK resolves their links to the destination; see Links in marketing email. |
logLevel | error | none, error, warning, info or debug. |
openDeferredDeepLinks | false | Let the SDK open a deferred link’s in-app URL itself. |
deferredDeepLinkTimeout | 5 s | How long the first launch waits for Appy’s answer before reporting AppyError.timeout. |
launchLinkWaitInterval | 500 ms | How long a launch waits for a link, so a cold start from a link sends one request. |
requestTimeout | 10 s | Timeout per HTTP attempt. |
apiBaseUrl | https://api.appy.to | Change only for staging. |
android.collectInstallReferrer | true | Read the store’s install referrer on the first Android launch. |
android.metaAppId | none | Your Facebook App ID, to read the Meta Install Referrer. |
A link that opens the app before configure runs, such as the link of a cold start, is kept by the native SDK and resolved right after it. A second configure call in the same process, for example after a hot restart, is ignored.
Handle deep links
Appy.onDeepLink is a broadcast stream of the sealed class AppyDeepLinkResult, so a switch covers every case:
Appy.onDeepLink.listen((result) {
switch (result) {
case AppyDeepLinkFound(:final link):
final item = link.parameters['item'];
router.go(item != null ? '/items/$item' : '/campaigns/${link.slug}');
case AppyDeepLinkNotFound() || AppyDeepLinkFailed():
break;
}
});| Result | When |
|---|---|
AppyDeepLinkFound(link) | An Appy link opened the app, or on the first launch Appy found the link tapped before the install (link.isDeferred is true). |
AppyDeepLinkNotFound() | First launch only. No Appy link led to this install. |
AppyDeepLinkFailed(error) | First launch only. AppyError.timeout, AppyError.network or AppyError.invalidResponse, for example an invalid key or a plan without the SDK. |
- The first launch after install emits exactly once. Every Appy link that opens the app, cold or warm, emits
AppyDeepLinkFound. Other launches emit nothing. - A result that arrives while nothing listens is kept by the native SDK and delivered once to the next listener. Listen from a widget or service that lives as long as the app.
- An
AppyDeepLinkhasurl,deeplinkUrl,slug,parameters,isDeferredandclickedAt. When Appy cannot be reached, a link that opened the app still arrives, read from its own URL, withoutdeeplinkUrlandclickedAt. Appy.getLatestDeepLink()returns the most recent link, including a deferred link that arrived after the timeout.- Links from push notifications, in-app messages or QR scanners: pass the URL to
Appy.handleUrl(uri). It returnstruefor an Appy link, which then arrives ononDeepLink.
Other link plugins
If you also use a plugin such as app_links for other domains, it may receive Appy links as well. Ignore your Appy link domain there and route Appy links only from onDeepLink.
Deferred deep links
On the first launch after install, the plugin asks Appy for the link tapped before the install. onDeepLink emits AppyDeepLinkFound with isDeferred == true, or AppyDeepLinkNotFound for an organic install, within deferredDeepLinkTimeout counted from configure. With openDeferredDeepLinks: true the SDK also opens the link’s deeplinkUrl, which your app receives like any other link to its own scheme; route deferred links in one of the two places, not both. Deferred deep links in the iOS guide explains timing and testing.
Install attribution
Appy.onAttribution.listen((attribution) {
analytics.setUserProperty(name: 'acquisition', value: attribution.isOrganic ? 'organic' : attribution.campaign ?? attribution.linkSlug);
});
final stored = await Appy.getAttribution();onAttribution emits once, when Appy answers the first launch, also after a deep link timeout. getAttribution() returns the stored value on later launches. An AppyAttribution has isOrganic, linkSlug, linkTitle, source, medium, campaign, clickedAt and network for installs that Google Ads or Meta attributed. Apple Ads needs no setup; for Meta, set AppyAndroidOptions(metaAppId: …). Check attribution.verified from your backend before granting rewards, as described in Verified installs.
Track events
await Appy.track('signup');
await Appy.track('add_to_cart', properties: {'sku': 'A1', 'qty': 2});
await Appy.track('purchase', revenue: 19.98, currency: 'USD', deduplicationId: purchase.purchaseID);Property values are String, num or bool; other values are dropped. Pass the store’s transaction ID as deduplicationId, for example PurchaseDetails.purchaseID from in_app_purchase, and Appy counts the purchase once, also when your backend reports it with the same ID. The limits and the sending schedule are in Track events.
Users, privacy and consent
await Appy.setUserId('u_123');
if (!consent.hasAnswered) {
await Appy.setTrackingEnabled(false);
}
await Appy.configure(configuration);setUserId attaches an opaque user ID to later opens and events; pass null on sign-out. setTrackingEnabled(false) is stored across launches and stops the launch open, the deferred deep link lookup and events until you turn it back on, for example when the user consents. See Privacy and consent.
Build share links
final url = await Appy.buildLink('spring', parameters: {'ref': referralCode});The result is https://acme.appy.to/spring?ref=… on the first link domain, or null before configure, without a link domain or for an invalid slug.
Test and debug
xcrun simctl openurl booted "https://acme.appy.to/spring?item=42"
adb shell 'am start -W -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "https://acme.appy.to/spring?item=42"'| Symptom | Check |
|---|---|
Your router shows a “not found” page for /spring | Flutter’s deep linking is still on: FlutterDeepLinkingEnabled on iOS, flutter_deeplinking_enabled on Android. |
| Links open the browser | The association files for your domain and the Team ID or SHA-256 fingerprint of the build you run. See Test and debug. |
onDeepLink never emits | configure runs at startup, the domain is in linkDomains, and something listens to the stream. |
AppyDeepLinkFailed(AppyError.invalidResponse) | HTTP 401 or 403 in the log, with logLevel: AppyLogLevel.debug: the publishable key, or a plan without SDK access. |
API reference
Appy | Description |
|---|---|
configure(AppyConfiguration) | Starts the SDK. Call once, early. |
onDeepLink | Broadcast stream of AppyDeepLinkResult. |
onAttribution | Broadcast stream of AppyAttribution, once per install. |
getAttribution(), getLatestDeepLink() | Stored attribution and the most recent link, or null. |
handleUrl(Uri) | Resolves an Appy link received another way. |
track(name, {properties, revenue, currency, deduplicationId}) | Queues an event. |
setUserId(String?) | Sets or clears the user ID. |
setTrackingEnabled(bool), isTrackingEnabled() | Stored opt-out, on by default. |
buildLink(slug, {parameters}) | Builds a share link. |
flush() | Sends queued events now. |
nativeSdkVersion(), pluginVersion | Versions of the native SDK and the plugin. |