Flutter Deep Linking: Universal Links, App Links, and Deferred Deep Linking (2026)

Flutter Deep Linking: Universal Links, App Links, and Deferred Deep Linking (2026)

Key takeaways

  • Flutter deep linking has three layers: the OS layer (AASA / assetlinks.json), the engine layer (your Navigator / GoRouter / AutoRoute config), and the intelligence layer (deferred matching, attribution, social-browser handling).
  • The flutter_ulink_sdk package handles AASA + assetlinks.json hosting, deferred deep linking, and parameter passthrough — without you touching AppDelegate or AndroidManifest.xml for hostname configuration.
  • The public API is small: ULink.instance.initialize(ULinkConfig(...)), then listen on onUnifiedLink (or onDynamicLink for the legacy v1 stream).
  • The ULinkAutoRouteTransformer plugs straight into AutoRoute's deepLinkTransformer — return a route path string from the resolver and AutoRoute handles the rest.
  • The biggest gotcha is testing: the AASA cache, in-app browsers (Instagram/TikTok), and cold-start vs. warm-start link delivery all need explicit verification.

Why Flutter teams care about deep linking

Flutter's value prop — one codebase, two stores — runs into the wall the moment a user comes from outside the app. A push notification, a social share, an SMS — every external entry point needs to map cleanly to a screen in your widget tree. And it has to do that whether the app is foregrounded, backgrounded, or not yet installed.

In 2026, "deep linking" in Flutter means more than Navigator.pushNamed. It means owning the AASA file, surviving the App Store handoff with parameters intact, and routing reliably from the in-app browsers of Meta and ByteDance. That's where a managed deep-linking platform pays for itself.

This guide walks through the architecture and a working flutter_ulink_sdk integration, including AutoRoute.


The three layers of Flutter deep linking

1. The OS layer — the gateway

Before Flutter sees a URL, the operating system has to decide your app is the right destination.

  • iOS (Universal Links): the system reads apple-app-site-association from your domain at app install time and on a refresh schedule. The file lists the team ID + bundle ID authorized to handle URLs on that domain.
  • Android (App Links): the system reads assetlinks.json from https://yourdomain.com/.well-known/assetlinks.json. The file ties your package name and SHA-256 signing fingerprint to that domain.

Misconfigure either file and the OS silently falls back to the browser. Ulinkly hosts both files for you on a CDN.

2. The Flutter engine layer — the handoff

Once the OS dispatches the URL to your app, Flutter receives it through the standard platform channels. Whether you use Navigator, go_router, or auto_route for routing, your job at this layer is to parse the URL and dispatch to the right screen.

The wrinkle: cold start vs. warm start. On a cold start the link arrives via a one-shot fetch; on a warm start it streams through a Stream. Your handler needs to listen to both.

3. The Ulinkly layer — the intelligence

Standard Flutter routing fails the moment the user doesn't have the app installed yet. Ulinkly's role is to:

  • Persist the original click intent through the App Store install
  • Match the install back to the click and hand parameters to your app on first launch
  • Detect Instagram/TikTok in-app browsers and force the OS handoff
  • Capture analytics without leaking PII

Step-by-step: integrating flutter_ulink_sdk

1. Add the dependency

dependencies:
  flutter:
    sdk: flutter
  flutter_ulink_sdk: ^0.2.19

Check pub.dev/packages/flutter_ulink_sdk for the latest published version.

Run flutter pub get.

2. Initialize the SDK

Initialize once, early — typically in main() or your top-level StatefulWidget's initState. Pass an API key from the Ulinkly dashboard.

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

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await ULink.instance.initialize(ULinkConfig(
    apiKey: 'ulk_your_api_key_here',
    debug: true, // verbose logs in dev — drop in release
  ));

  runApp(const MyApp());
}

3. Listen for incoming links

Ulinkly exposes two streams. Most teams only need onUnifiedLink:

import 'dart:async';
import 'package:flutter_ulink_sdk/flutter_ulink_sdk.dart';

class DeepLinkHandler {
  StreamSubscription<ULinkResolvedData>? _subscription;

  void start() {
    _subscription = ULink.instance.onUnifiedLink.listen((data) {
      final slug = data.slug;
      final params = data.parameters ?? {};
      final screen = params['screen'];
      final productId = params['productId'];

      if (screen == 'product' && productId != null) {
        appRouter.pushNamed('/product/$productId');
      }
    });
  }

  void dispose() {
    _subscription?.cancel();
  }
}

4. Check for deferred links on first launch

For brand-new users coming from a campaign, call checkDeferredLink() after initialization. Ulinkly matches the install against the original click and emits a ULinkResolvedData event on onUnifiedLink if there's a hit.

await ULink.instance.initialize(config);
await ULink.instance.checkDeferredLink();

That's the entire deferred deep linking surface — the SDK handles the matching and surfaces the link through the same stream you already listen on.

5. (Optional) Handle reinstalls

If you want a different experience for a returning install vs. a brand-new one, listen on onReinstallDetected:

ULink.instance.onReinstallDetected.listen((info) {
  // info.previousInstallationId, info.detectionMethod
  // useful for win-back flows
});

AutoRoute integration

auto_route is the dominant strongly-typed router in production Flutter apps. The flutter_ulink_sdk ships a transformer that plugs into AutoRoute's deepLinkTransformer slot.

The resolver receives ULinkResolvedData and returns a route path string (not a route object). AutoRoute resolves that path against your route table.

import 'package:auto_route/auto_route.dart';
import 'package:flutter_ulink_sdk/flutter_ulink_sdk.dart';

final _appRouter = AppRouter();

MaterialApp.router(
  routerConfig: _appRouter.config(
    deepLinkTransformer: ULinkAutoRouteTransformer(
      routeResolver: (data) {
        final params = data.parameters ?? {};
        final type = params['type'];

        if (type == 'sale') {
          return '/sales/${params['id']}';
        }
        if (type == 'product') {
          return '/products/${params['productId']}';
        }
        return null; // fall through to default behavior
      },
      debugMode: kDebugMode,
    ).transformUri,
  ),
);

Returning null from the resolver lets AutoRoute fall back to its default URL-to-route mapping — useful when you want most paths to "just work" and only intercept the ones that need custom logic.


Why a managed platform beats DIY

Most teams that try to build Flutter deep linking themselves discover the same potholes within a few months:

  • In-app browsers. Instagram and TikTok IABs strip Universal Link intents and silently load a mobile web view. Force-open scripts have to be maintained as those apps update. (How Ulinkly handles walled-garden browsers.)
  • Deferred matching. Connecting an install back to a click without using banned fingerprinting requires a probabilistic + deterministic matching engine that's a real backend project on its own.
  • AASA hosting. A misconfigured Cache-Control header or an expired cert silently breaks every deep link until someone notices the conversion drop.
  • Reinstall detection. Useful, hard, and not something you want to roll on your own.

Ulinkly absorbs all of this so the only Flutter code you write is the listener and the router glue.


Debugging Flutter deep links

A few practical tips:

  1. Use the Link Validator. The Ulinkly dashboard checks your AASA, assetlinks.json, and SHA-256 fingerprint in one click. If those three are green, 90% of "my link doesn't work" problems are eliminated.
  2. Test cold start. Kill the app via the system tray, then tap a link. Many bugs only surface on cold start because that path uses a one-shot fetch instead of the stream.
  3. Test on a real device. Universal Links don't reliably trigger from the iOS Simulator. Use a TestFlight build or flutter run --release on hardware.
  4. Test through the IAB. Open Instagram, paste a link in your DMs, tap from there. If it falls back to a mobile browser, you'll see it in the dashboard's click log.
  5. Watch the debug overlay. flutter_ulink_sdk ships a ULinkDebugOverlay widget — drop it in MaterialApp.builder during development to see live link events.

FAQ

Does this work with go_router or vanilla Navigator?

Yes. flutter_ulink_sdk is router-agnostic. The onUnifiedLink stream gives you a ULinkResolvedData object — pass data.slug or data.parameters to whatever router you use. The ULinkAutoRouteTransformer is convenience for AutoRoute users specifically.

Do I still need to configure AppDelegate.swift and AndroidManifest.xml?

You need the Associated Domains entitlement on iOS and the App Links intent filter on Android — both standard Universal Link / App Link plumbing. Ulinkly hosts the AASA and assetlinks.json files; you still register your domain in those two manifest spots. The dashboard generates the exact snippets to copy in.

How do I test deferred deep linking?

The simplest path: create a test Ulinkly, click it in Safari (logged in to a fresh device or emulator without your app installed), get redirected to the App Store, install your debug build, open it, and confirm the listener fires with the correct parameters. The dashboard's "Recent Clicks" view confirms the match server-side.

Does Ulinkly support flutter_dynamic_links or other FDL forks?

Ulinkly is a successor to FDL, not a fork of it. Migration from firebase_dynamic_links is a SDK swap — see the FDL alternative guide for the side-by-side.

What about Flutter web?

Ulinkly links resolve to web URLs by design. On Flutter web, you receive the URL through the browser as you would any web app, and you can read the parameters from the URL directly. The SDK is a mobile-first integration.

Is there a free tier for Flutter projects?

Yes — see ulink.ly/pricing. Most early-stage Flutter apps fit within the free tier.


Ship Flutter deep links that survive the App Store and the in-app browser. Create a free Ulinkly account, add flutter_ulink_sdk from pub.dev, and follow the docs walkthrough.

Read More Articles

Explore more guides and insights on deep linking and mobile development.

Back to Blog