Get started

Flutter

The unolink package is a thin layer over the native iOS and Android SDKs, which ship inside it. It needs no permissions beyond internet and no Info.plist entries. Published on pub.dev by unolink.dev. Pro plan and up.

Requirements

  • Flutter 3.22+ (Dart 3.4+).
  • Android 6.0+ (minSdk 23) and iOS 13+.
  • An SDK key: in the dashboard, open your app → Settings → SDK keys → New key. It's shown once, so copy it then.

Install

flutter pub add unolink

That adds unolink: ^0.3.3 to pubspec.yaml and fetches it. Rebuild the app afterwards; a hot reload isn't enough after adding native code.

If CocoaPods fails in the iOS build with an encoding error (Ruby 4, or a locale that isn't UTF-8), run it yourself in UTF-8, then build again:

cd ios && LANG=en_US.UTF-8 pod install

Configure

Call configure once in main(), after WidgetsFlutterBinding.ensureInitialized() and before runApp:

import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:unolink/unolink.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Unolink.configure('ulk_…', debug: kDebugMode);
  runApp(const MyApp());
}

Options

OptionDefaultWhat it does
debugfalseMarks this install as a test device. Pass kDebugMode. Dashboards hide test devices unless Include test devices is on.
logLevelUnolinkLogLevel.warnHow much goes to the Xcode console or Logcat (tag Unolink): none, error, warn, info, debug.
baseUrlhttps://api.unolink.devThe SDK API. Leave it as it is.

The key is public: it ships inside your app and can only report data for that one app. See SDK keys.

Funnel events

Add one line at each moment that matters. These become the steps in Funnel and Journeys.

Unolink.onboardingCompleted();                     // the last onboarding screen is done
Unolink.paywallViewed(placement: 'onboarding');    // a paywall appeared; placement says which one
Unolink.paywallSkipped(placement: 'onboarding');   // closed without buying
Unolink.signUp(method: 'apple');                   // an account was created
Unolink.signUpSkipped();                           // went on as a guest (0.3.2+)

Without RevenueCat, report trials and purchases yourself. The amount is a string so it never rounds:

Unolink.trialStarted(productId: 'pro_yearly');
Unolink.purchase('49.99', 'USD', productId: 'pro_yearly');
Unolink.purchaseCancelled(productId: 'pro_yearly');              // closed the purchase sheet
Unolink.purchaseFailed(productId: 'pro_yearly', reason: 'payment_failed');

Your own events take a name and up to 20 properties:

Unolink.track('level_up', {'level': 3});

The full list, with the names that reach the dashboard, is in Events.

User ID and RevenueCat

If you use RevenueCat, connect it under your app → Settings → RevenueCat. Then don't call purchase or trialStarted: RevenueCat sends trials, purchases, renewals, cancellations and refunds itself, and Unolink ignores app purchase calls so nothing counts twice.

To join those purchases to the install, report RevenueCat's own app user ID. With SDK 0.3.2 or later, one line keeps it current. Call it once at launch, before or after configure:

Unolink.connectRevenueCat(
  () => Purchases.appUserID,
  addListener: Purchases.addCustomerInfoUpdateListener,
  removeListener: Purchases.removeCustomerInfoUpdateListener,
);

Unolink reads the ID once configured, on every customer info update (purchases, logIn, logOut) and whenever the app resumes, and sends it only when it changed. If RevenueCat isn't configured yet, the read fails quietly and is tried again later. It returns a function that disconnects. So Unolink.revenueCatUserChanged() is normally not needed on Flutter.

Without it (or before 0.3.2), call setUserId with RevenueCat's ID after Purchases.configure, after every Purchases.logIn and Purchases.logOut, and on every launch. Use one or the other, not both:

// after Purchases.configure, logIn and logOut, and on every launch
Unolink.setUserId(await Purchases.appUserID);

For a guest, that's RevenueCat's anonymous ID, so a purchase made before signing up joins the install right away. After logIn, it's the account's ID. Every ID an install has reported counts, so the guest's purchase stays joined. See Guests and accounts.

Unolink never stores the ID itself, only a one-way hash that's different for each of your apps.

On Android, when someone installs from a Unolink link that has a deep link, the listener runs once, on first open. On iPhone it isn't called: Apple has no install referrer, and Unolink never fingerprints.

Unolink.setDeferredDeepLinkListener((link) {
  // link.deepLink: '/promo/3for2', link.params: {'offer': '3for2'}
  // link.linkSlug: 'tiktok' or null, link.clickId: 'clk_01JA…'
  if (link.deepLink != null) router.go(link.deepLink!);
  Unolink.consumeDeferredDeepLink();
});

The listener is called once per install. A link that arrived before you set it is delivered when you set it.

Open it after onboarding

First opens usually start in onboarding. If the link's screen sits behind onboarding or sign-in, don't open it from the listener. When onboarding (or sign-in) is done, read the pending link, open it, then consume it:

// when onboarding (or sign-in) is done, and on every launch after that until it's consumed
final link = await Unolink.getPendingDeferredDeepLink();
if (link?.deepLink != null) {
  router.go(link!.deepLink!);
  await Unolink.consumeDeferredDeepLink();
}

The link waits until you consume it, even if the app is closed in between (up to 30 days after the install; 0.3.3+). A new user who quits halfway through onboarding still gets it on the next launch, so you don't need to store it yourself. Call consumeDeferredDeepLink() once you've opened it: until then, getPendingDeferredDeepLink() returns it on every launch. It's null before configure and until Google Play has answered, and always null on iPhone.

Set the link's Deep link and Parameters in the dashboard (Links → ⋯ → Edit). How it works is in Android deferred deep links.

Other calls

CallWhat it does
await Unolink.getInstallId()The random ID of this install, or null before configure. It identifies no person or device.
Unolink.flush()Sends queued events now. Normally not needed.
Unolink.setEnabled(false)Turns tracking off and deletes queued events, for your opt-out setting. true turns it back on.

Test devices

With debug: kDebugMode, debug builds are test devices. Their data is real but hidden until you turn on Include test devices in Funnel and Journeys. More in Testing.

Privacy

  • No IDFA, no Google Advertising ID, no App Tracking Transparency prompt, no fingerprinting.
  • A random install ID, app version, OS version, SDK version, phone or tablet, language. Country comes from the network edge. IP addresses are never stored.
  • On iOS the native SDK's privacy manifest comes with the package: analytics, not linked to identity.
  • Never put names, emails or other personal data in event properties.

Troubleshooting

  • MissingPluginException? Stop the app and run it again. Hot reload and hot restart don't load new native code.
  • Nothing in the dashboard? Turn on Include test devices: kDebugMode builds are hidden by default.
  • Still nothing? Configure with logLevel: UnolinkLogLevel.debug and look for Unolink in the Xcode console or Logcat. A wrong or revoked key is logged as an error.

More in Troubleshooting.