dynalink_flutter 0.0.18
dynalink_flutter: ^0.0.18 copied to clipboard
Flutter plugin for Dynalink
DynaLink Flutter Plugin #
Deep links, dynamic links and install attribution for Flutter apps, backed by dynalink.app.
The plugin captures every way a user can reach your app through a DynaLink —
a tap on a verified App Link, a deep link through the loading page, a fresh
install from the Play Store or the App Store — and hands you a single
DynalinkEvent carrying the destination URL, the campaign context and the
ad-network click IDs.
Features #
- Deep link handling — one stream for every entry path, cold start included.
- Install attribution — Play Install Referrer on Android, clipboard and device fingerprint matching on iOS.
- Click IDs —
gclid,gbraid,fbclid,ttclid,twclid,li_fat_idcaptured in the browser before install and delivered to the app. - Campaigns — create, list and read campaign stats; UTM context travels with the link.
- Link creation — short links and dynamic links from the app.
- Android + iOS, with App Links / Universal Links and custom domains.
Step 1 — Create the project #
Create your project at dynalink.app and note the project ID, the project prefix and the project key. Keep the key out of source control.
Step 2 — Install the plugin #
dependencies:
dynalink_flutter: ^0.0.18
flutter pub get
Step 3 — Configure the platforms #
Android #
Add both intent filters to your activity in AndroidManifest.xml:
<!-- Loading page → app, when the OS did not open the app itself -->
<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="dynalink-{projectId}" android:host="dynalink.app" />
</intent-filter>
<!-- Verified App Links: the OS opens the app directly -->
<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="{projectPrefix}.dynalink.app" />
</intent-filter>
Then add your Asset Links file in the Admin Panel (Project → Settings) —
see the Android App Links docs.
DynaLink serves it at https://{projectPrefix}.dynalink.app/.well-known/assetlinks.json.
What
android:autoVerify="true"changes. Once Android has verified yourassetlinks.json,https://{projectPrefix}.dynalink.app/{code}opens the app directly — the browser and the DynaLink loading page are skipped. The app then receives the short URL, not the destination: the SDK resolves it throughGET /api/links/{code}and emits it asDynalinkEvent.actualUrl, and reports the click itself (POST /api/links/{code}/click) so click counts, unique visitors and geo data stay correct.
- Always navigate with
event.actualUrlfrom the stream — never by parsing the incoming URL path yourself.- Leave Flutter's own deep linking off. If your manifest declares
<meta-data android:name="flutter_deeplinking_enabled" android:value="true" />, your router will also try to navigate to/{code}; remove it or set it tofalseso DynaLink owns the link handling.
iOS #
In ios/Runner/Info.plist:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>{bundleId}</string>
<key>CFBundleURLSchemes</key>
<array>
<string>dynalink-{projectId}</string>
</array>
</dict>
</array>
<key>NSUserActivityTypes</key>
<array>
<string>NSUserActivityTypeBrowsingWeb</string>
</array>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>dynalink-{projectId}</string>
</array>
<key>CFBundleAssociatedDomains</key>
<array>
<string>applinks:{projectPrefix}.dynalink.app</string>
</array>
<key>FlutterDeepLinkingEnabled</key>
<false/>
Then add the Apple App Site Association file in the Admin Panel (Project → Settings) — see the Associated Domains docs.
Custom domains #
If your links are served from your own host (https://links.yourbrand.com/{code}),
declare it in the Admin Panel, point it at DynaLink, and list it in the manifest
entries above as well — Android and Apple fetch the association files from the
exact host that serves the link. Then pass it to initialize (see below), or let
the SDK resolve it from the API on the first unknown host it sees.
Step 4 — Initialize #
import 'package:dynalink_flutter/dynalink_flutter.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Dynalink.initialize(
projectId: '{projectId}',
publicKey: '{projectKey}',
// Optional — hosts other than {projectPrefix}.dynalink.app that serve your
// links. Declaring them makes link handling work on the very first link and
// without a network round-trip.
customDomains: const ['links.yourbrand.com'],
);
runApp(const MyApp());
}
Call it before runApp so no incoming link is missed: the stream replays the
last event, so a listener attached later still receives it.
Handling links #
dynamicLinkStream emits a DynalinkEvent every time a link is resolved —
whether the OS opened the app directly, the loading page redirected into it, or
the install was just attributed after a Play Store / App Store download.
Dynalink.instance.dynamicLinkStream.listen((DynalinkEvent event) {
// Where the user should land
navigateTo(event.actualUrl);
if (event.hasCampaign) {
print('Campaign ${event.campaignName} (${event.campaignId})');
print('UTM ${event.utmSource} / ${event.utmMedium} / ${event.utmCampaign}');
}
if (event.hasClickIds) {
if (event.gclid != null) forwardToGoogleAds(event.gclid!);
if (event.fbclid != null) forwardToMetaCapi(event.fbclid!, event.fbclidCapturedAt);
if (event.ttclid != null) forwardToTikTok(event.ttclid!);
if (event.twclid != null) forwardToXAds(event.twclid!);
if (event.liFatId != null) forwardToLinkedIn(event.liFatId!);
}
});
DynalinkEvent #
| Field | Type | Notes |
|---|---|---|
actualUrl |
String |
Destination to navigate to — always set |
campaignId, campaignName |
int?, String? |
Campaign the link belongs to |
utmSource, utmMedium, utmCampaign, utmTerm, utmContent |
String? |
URL-level values win over the campaign defaults |
gclid, gbraid, fbclid, ttclid, twclid, liFatId |
String? |
Ad-network click IDs |
fbclidCapturedAt |
DateTime? |
When the fbclid was first seen, for CAPI dedup windows |
attributedAt |
DateTime? |
When the backend confirmed the install match |
hasCampaign, hasClickIds |
bool |
Convenience getters |
Breaking change in 0.0.13 — the stream emits
DynalinkEvent, notString. Replacelisten((String? url) => …)withlisten((DynalinkEvent event) => …)and readevent.actualUrl.
Attribution outside the stream #
Values from the last attributed link are persisted, so you can read them even if no listener was attached at the time:
final campaignId = await Dynalink.instance.getLastCampaignId();
final gclid = await Dynalink.instance.getLastGclid();
// also: getLastGbraid, getLastFbclid, getLastTtclid, getLastTwclid, getLastLiFatId
Or query the attribution API directly:
final byCode = await Dynalink.instance.getAttributionByCode('ERTIN5');
final byFp = await Dynalink.instance.getAttributionByFingerprint(fingerprint);
Creating links #
final url = await Dynalink.instance.createShortenedLink(
CreateDynalinkForm(
url: 'https://yourapp.com/property/42', // required — the destination
isDeepLink: true, // open the app when installed
androidUrl: 'https://play.google.com/store/apps/details?id=com.yourapp',
iosUrl: 'https://apps.apple.com/app/id123456789',
fallbackUrl: 'https://yourapp.com', // no app, no store link
socialTitle: 'A place in Douala',
socialDescription: 'Three rooms, sea view',
socialImage: 'https://yourapp.com/og/42.png',
campaignId: 4, // optional
),
);
print(url); // https://{projectPrefix}.dynalink.app/ERTIN5
Campaigns #
final campaigns = await Dynalink.instance.getCampaigns(status: 'active');
final campaign = await Dynalink.instance.createCampaign(
CreateCampaignForm(
name: 'Summer 2026',
utmSource: 'google',
utmMedium: 'cpc',
utmCampaign: 'summer_2026',
startDate: DateTime(2026, 6, 1),
),
);
await Dynalink.instance.updateCampaign(campaign.id, form);
await Dynalink.instance.deleteCampaign(campaign.id);
final stats = await Dynalink.instance.getCampaignStats(campaign.id, days: 30);
print('${stats?.totalClicks} clicks on ${stats?.totalLinks} links');
Links attached to a campaign inherit its UTM parameters, which then come back on
DynalinkEvent.
Testing #
Three debug helpers replay each entry path against the real API, so you can test without a store install:
// Android Install Referrer / iOS clipboard — pass a URL with actual_url on it
await Dynalink.debugSimulateInstallReferrer(
'https://dynalink.app?dyna_code=ERTIN5&actual_url=https%3A%2F%2Fyourapp.com&gclid=Cj0KCQj',
);
// iOS fingerprint matching — the fingerprint must already exist server-side
await Dynalink.debugSimulateIOSFingerprint('102.244.220.125-iOS-18.1-390x840');
// App already installed, opened by a verified App Link / Universal Link
await Dynalink.debugSimulateDeepLink('https://yourprefix.dynalink.app/ERTIN5');
To test the real Play Store install referrer, follow this walkthrough.
Nothing arrives on the stream? #
The SDK logs every step. Check for:
fetchPendingLinkByCode(...) failed— the code was not resolved: wrong project key, link belonging to another project, or no network.Ignoring link that is not served by DynaLink— the host is neitherdynalink.appnor a declared custom domain; add it tocustomDomains.- Nothing at all on Android — the App Links verification may not have
happened: check
adb shell pm get-app-links {yourPackage}.
Use cases #
- Route users to the exact screen a shared link points to.
- Attribute installs to the ad, campaign and channel that drove them.
- Share content with short, branded links carrying social preview metadata.