Key takeaways
- Flutter deep linking has three layers: the OS layer (AASA /
assetlinks.json), the engine layer (yourNavigator/GoRouter/AutoRouteconfig), and the intelligence layer (deferred matching, attribution, social-browser handling). - The
flutter_ulink_sdkpackage handles AASA +assetlinks.jsonhosting, deferred deep linking, and parameter passthrough — without you touchingAppDelegateorAndroidManifest.xmlfor hostname configuration. - The public API is small:
ULink.instance.initialize(ULinkConfig(...)), then listen ononUnifiedLink(oronDynamicLinkfor the legacy v1 stream). - The
ULinkAutoRouteTransformerplugs straight into AutoRoute'sdeepLinkTransformer— 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-associationfrom 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.jsonfromhttps://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-Controlheader 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:
- 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. - 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.
- Test on a real device. Universal Links don't reliably trigger from the iOS Simulator. Use a TestFlight build or
flutter run --releaseon hardware. - 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.
- Watch the debug overlay.
flutter_ulink_sdkships aULinkDebugOverlaywidget — drop it inMaterialApp.builderduring 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.
