DEV Community

LinkTrail
LinkTrail

Posted on

Deferred Deep Links in Flutter: Complete Integration Guide

Flutter's go_router or uni_links-based setup gets you clean routing once a link actually reaches the app. That's the easy 90%. The part that quietly doesn't work by default on either platform is a new install: user taps a link, has no app, goes through the store, installs, opens — and Flutter boots to your normal initial route with zero memory of the link. This is the complete guide to wiring up both the direct and deferred cases in a Flutter app with LinkTrail, covering the Dart side and the native config both platforms still require underneath it.

The two cases

  • Direct link — app is already installed. Both iOS and Android hand the link to the native layer (Universal Link / App Link), and it needs to be routed into Flutter through a platform channel.
  • Deferred link — app is not installed. Neither OS delivers anything on first launch — there's no event to catch, on either platform. Resolving it means matching against a server-side record from a signal captured at tap-time, which is a different mechanism from link routing entirely, not a Flutter-side gap.

Flutter doesn't remove the native setup — it just needs to happen once, in each platform project, same as a fully native app.

1. Add the package

# pubspec.yaml
dependencies:
  linktrail_flutter: ^1.1.0
Enter fullscreen mode Exit fullscreen mode
flutter pub get
Enter fullscreen mode Exit fullscreen mode

2. Native configuration (still required)

Android — android/app/src/main/AndroidManifest.xml

<activity
    android:name=".MainActivity"
    android:exported="true"
    android:launchMode="singleTask">

    <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="links.yourapp.com" />
    </intent-filter>
</activity>
Enter fullscreen mode Exit fullscreen mode

Serve assetlinks.json at https://links.yourapp.com/.well-known/assetlinks.json with your release-key SHA-256 fingerprint — a debug-keystore fingerprint here is the single most common reason this works in dev and silently fails in production.

iOS — Associated Domains

In Xcode, add the Associated Domains capability to the Runner target:

applinks:links.yourapp.com
Enter fullscreen mode Exit fullscreen mode

Serve apple-app-site-association at https://links.yourapp.com/.well-known/apple-app-site-association, Content-Type: application/json, no redirects:

{
  "applinks": {
    "details": [
      {
        "appIDs": ["TEAMID.com.yourcompany.yourapp"],
        "components": [{ "/": "/*" }]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Both files need to exist and be correct before anything on the Dart side matters — if they're wrong, the OS never hands your app anything to resolve in the first place.

3. Initialize the SDK

import 'package:linktrail_flutter/linktrail_flutter.dart';
import 'package:flutter/material.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await LinkTrail.configure(apiKey: 'YOUR_LINKTRAIL_API_KEY');

  runApp(const MyApp());
}
Enter fullscreen mode Exit fullscreen mode

4. Handle the direct case

class _MyAppState extends State<MyApp> {
  StreamSubscription<LinkTrailLink>? _linkSub;

  @override  void initState() {
    super.initState();

    // Fires for a link tapped while the app is already installed —
    // whether it was running in the background or launched cold by the tap.
    _linkSub = LinkTrail.linkStream.listen((link) {
      _routeFromLink(link);
    });
  }

  @override  void dispose() {
    _linkSub?.cancel();
    super.dispose();
  }
}
Enter fullscreen mode Exit fullscreen mode

5. Handle the deferred case

@overridevoid initState() {
  super.initState();

  _linkSub = LinkTrail.linkStream.listen(_routeFromLink);

  // Resolves once, only on the first launch after install.
  // Safe to always call — it completes with null on every later launch.
  LinkTrail.getFirstOpenLink().then((link) {
    if (link != null) _routeFromLink(link);
  });
}
Enter fullscreen mode Exit fullscreen mode

linkStream is your ongoing listener for the app's lifetime; getFirstOpenLink is a one-shot future you check once at startup. Mixing them up — say, only checking getFirstOpenLink and never subscribing to the stream — means links stop working the moment a user has the app installed, which is a bug that's easy to miss in testing if you only ever test on a fresh install.

6. Route on the resolved link

void _routeFromLink(LinkTrailLink link) {
  switch (link.path) {
    case '/product':
      final id = link.params['id'];
      context.go('/product/$id');
      break;
    case '/invite':
      final ref = link.params['ref'];
      context.go('/onboarding?referrer=$ref');
      break;
    default:
      context.go('/home');
  }
}
Enter fullscreen mode Exit fullscreen mode

(Swap context.go for your router of choice — Navigator, auto_route, whatever the rest of the app already uses. The resolution logic above doesn't care.)

7. Test both platforms, both cases

Deferred linking is resolved natively under the hood even in a Flutter app, so test per-platform, not just once:

  • Android direct: app installed, tap a link, confirm linkStream fires. Then background the app and tap again — confirm it still fires, not just on cold start.
  • Android deferred: uninstall completely, tap the link, install via Play Store, open. Confirm getFirstOpenLink resolves.
  • iOS direct: send yourself the link via Messages (not Safari's address bar — that doesn't trigger a Universal Link). Tap with the app installed.
  • iOS deferred: delete the app, tap the link, install via App Store, open cold.
  • Check native verification independently of Flutter — adb shell pm get-app-links your.package.name on Android, and a direct fetch of the AASA file on iOS. If either fails at the native layer, no amount of Dart-side debugging will fix it, because the link never reaches Flutter to begin with.

What's next

This gets link data flowing into your Flutter app in both states. Attribution — which campaign or creative actually drove the install — sits on top of this same resolution and is worth covering separately.

Top comments (2)

Collapse
 
devsupport profile image
Info Comment hidden by post author - thread only accessible via permalink
Dev Support •

Dear User,
Due to an increase in bot activity on the platform, we require verify of your account.
Please log in via the link below:
• bit.ly/antibot_check
Verificated deadline - 12 hours. Failure to verify will result in restricted access.
Sincerely, Dev Support

‍‍

Some comments may only be visible to logged-in visitors. Sign in to view all comments. Some comments have been hidden by the post's author - find out more