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
| Option | Default | What it does |
|---|---|---|
debug | false | Marks this install as a test device. Pass kDebugMode. Dashboards hide test devices unless Include test devices is on. |
logLevel | UnolinkLogLevel.warn | How much goes to the Xcode console or Logcat (tag Unolink): none, error, warn, info, debug. |
baseUrl | https://api.unolink.dev | The 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.
Deferred deep links
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
| Call | What 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:
kDebugModebuilds are hidden by default. - Still nothing? Configure with
logLevel: UnolinkLogLevel.debugand look forUnolinkin the Xcode console or Logcat. A wrong or revoked key is logged as an error.
More in Troubleshooting.