dynalink_flutter 0.0.18 copy "dynalink_flutter: ^0.0.18" to clipboard
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 IDsgclid, gbraid, fbclid, ttclid, twclid, li_fat_id captured 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 your assetlinks.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 through GET /api/links/{code} and emits it as DynalinkEvent.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.actualUrl from 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 to false so 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.


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, not String. Replace listen((String? url) => …) with listen((DynalinkEvent event) => …) and read event.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);

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 neither dynalink.app nor a declared custom domain; add it to customDomains.
  • 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.