Appy

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

bash
flutter pub add appy_sdk
PlatformNative SDK
iOS with Swift Package Manager (the default since Flutter 3.44)Added automatically from the AppySDK Swift package.
iOS with CocoaPodsAdd 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.
AndroidAdded 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. 1

    iOS: Associated Domains

    In Xcode, open ios/Runner.xcworkspace, add the Associated Domains capability to the Runner target and enter applinks:acme.appy.to. While developing, add applinks:acme.appy.to?mode=developer too.

  2. 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. 3

    iOS: Flutter deep linking

    In ios/Runner/Info.plist, set FlutterDeepLinkingEnabled to NO.

  4. 4

    Android: intent filters

    In android/app/src/main/AndroidManifest.xml, add the App Link and custom-scheme filters below to MainActivity, together with flutter_deeplinking_enabled set to false. Keep Flutter’s default singleTop launch mode.

  5. 5

    Android: backups

    Exclude to.appy.sdk.xml from Auto Backup as shown in Keep the first launch a first launch, so a reinstall still gets its first launch.

AndroidManifest.xml
<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

main.dart
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());
}
OptionDefaultMeaning
publishableKeyrequiredYour 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.
logLevelerrornone, error, warning, info or debug.
openDeferredDeepLinksfalseLet the SDK open a deferred link’s in-app URL itself.
deferredDeepLinkTimeout5 sHow long the first launch waits for Appy’s answer before reporting AppyError.timeout.
launchLinkWaitInterval500 msHow long a launch waits for a link, so a cold start from a link sends one request.
requestTimeout10 sTimeout per HTTP attempt.
apiBaseUrlhttps://api.appy.toChange only for staging.
android.collectInstallReferrertrueRead the store’s install referrer on the first Android launch.
android.metaAppIdnoneYour 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.

Appy.onDeepLink is a broadcast stream of the sealed class AppyDeepLinkResult, so a switch covers every case:

dart
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;
  }
});
ResultWhen
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 AppyDeepLink has url, deeplinkUrl, slug, parameters, isDeferred and clickedAt. When Appy cannot be reached, a link that opened the app still arrives, read from its own URL, without deeplinkUrl and clickedAt.
  • 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 returns true for an Appy link, which then arrives on onDeepLink.

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.

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

dart
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

dart
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.

dart
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.

dart
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

bash
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"'
SymptomCheck
Your router shows a “not found” page for /springFlutter’s deep linking is still on: FlutterDeepLinkingEnabled on iOS, flutter_deeplinking_enabled on Android.
Links open the browserThe association files for your domain and the Team ID or SHA-256 fingerprint of the build you run. See Test and debug.
onDeepLink never emitsconfigure 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

AppyDescription
configure(AppyConfiguration)Starts the SDK. Call once, early.
onDeepLinkBroadcast stream of AppyDeepLinkResult.
onAttributionBroadcast 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(), pluginVersionVersions of the native SDK and the plugin.