Flutter deep linking without the plugin scavenger hunt.
Flutter gives you a URL. It does not claim your domain, verify it, or tell you anything about a user who installed the app because of a link they tapped before your app existed on their phone.
Flutter has no built-in deep linking beyond receiving a URL the platform hands it. Universal links, App Links, association files and the install gap are all native concerns, which is why most Flutter apps end up with a stack of plugins that each solve one part and none of them solve the deferred case. Ferry ships a federated plugin that wraps the native iOS and Android SDKs, so one Dart stream delivers direct opens and deferred first-install matches on both platforms.
One API, two platforms, no channel code.
The parts that are genuinely platform-specific stay in the native SDKs. What crosses the bridge is the result, not the logic.
- The platform work does not go away
Associated Domains on iOS and an autoVerify intent filter on Android are still required, and both still need a valid association file on the hostname. Ferry serves those files from the edge. Dart cannot do this part for you, and any package that implies otherwise is describing something else.
- One stream, both platforms
Ferry.onLink is a broadcast stream that emits for a universal link, an App Link, and a deferred first-install match. Your router gets one entry point instead of a Platform.isIOS branch, because both native SDKs spell the fields identically.
- The matching stays native
The plugin is a wrapper, not a reimplementation. Play Install Referrer reading, App Clip handoff, pasteboard detection and per-install match state run in the native SDKs, which is where those platform APIs actually live.
- A late subscriber still gets the link
A link that resolves before the first listener attaches is buffered and delivered when you subscribe, so a cold start that came from a link does not race your widget tree. Subscribe once near the root, not inside a screen.
- Failures stay quiet
configure, track, flush and handle degrade to doing nothing when the native side is unavailable. Client link creation is the deliberate exception: it reports a typed failure, because a share sheet with no link in it is worse than an error.
import 'package:ferry_link_flutter/ferry_link_flutter.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Ferry.configure(publicKey: 'pk_live_xxx');
// One stream for iOS and Android, direct opens and deferred first installs.
Ferry.onLink.listen((link) {
switch (link.data['screen']) {
case 'product':
router.showProduct(link.data['product_id'], deferred: link.isDeferred);
default:
router.showHome();
}
});
runApp(const AcmeApp());
} Install ferry_link_flutter from pub.dev, add the entitlement and intent filter once per platform, and this is the entire Dart side.
The cross-platform failures nobody warns you about.
Flutter hides the platform difference right up until deep linking, where the two platforms fail in completely different ways at completely different times.
| What goes wrong | What to do about it |
|---|---|
| A plugin delivers URLs but nothing arrives after an install. | URL-delivery packages only see links that actually reached the app. A visitor who did not have the app never produced one. Deferred matching needs a server that recorded the original open, which is not something a Dart package can do on its own. |
| Two link plugins fight over the same intent. | Both register native handlers and whichever consumes the intent first wins, which changes between builds. Pick one owner for incoming links. Ferry.handle returns whether it took the URL so you can forward the rest. |
| It works in debug and breaks in release on Android. | Release builds carry the Play App Signing certificate, so assetlinks.json must list that SHA-256. Debug verification passing tells you nothing about what users will get. |
| The link arrives before the router exists. | Do not subscribe inside a screen widget. Subscribe once near the root of the app and let the buffered first event drive navigation once the navigator is mounted. |
| Flutter's own deep link handling swallows the URL. | Flutter router integrations also listen for platform link events. Decide which one owns navigation, and route from the payload data map rather than parsing the path a second time. |
| The install gap is only tested in a simulator. | Fingerprint signals come from real devices, and the Play Install Referrer only exists for real Play installs. Test the deferred path on physical hardware with the app deleted first, on both platforms. |
Questions developers ask first.
Does Flutter support deep linking out of the box? +
Flutter can receive a URL that the platform hands it, and that is where its involvement ends. Claiming the domain, getting it verified, and recovering a destination after an app store install are native platform concerns that Flutter itself does not cover.
Do I still need Associated Domains and an intent filter? +
Yes. Those live in ios/Runner/Runner.entitlements and android/app/src/main/AndroidManifest.xml, and no Dart package can replace them. Ferry serves the apple-app-site-association and assetlinks.json files the platforms fetch, but the app still has to claim the hostname.
What replaced firebase_dynamic_links for Flutter? +
Nothing from Google. Firebase Dynamic Links stopped serving on August 25, 2025, which left Flutter apps without the deferred half of deep linking. Ferry covers the same jobs: route by platform, keep the payload through a store install, and attribute what follows.
Does the Flutter SDK reimplement matching in Dart? +
No. It is a federated plugin that wraps the native iOS and Android SDKs through platform channels. Install referrer reading, App Clip handoff, install state and retries all stay native, so behaviour matches a pure Swift or Kotlin integration.
Can I use it alongside another link plugin? +
You can, but only one handler can own an incoming URL. Ferry.handle returns true when Ferry took ownership, so you can pass anything it declines to your other router rather than having two plugins race for the same intent.
Which Flutter versions are supported? +
Flutter 3.10 and Dart 3.0 or later, with the native minimums of iOS 15 and Android API 24 inherited from the underlying SDKs.
Stop gluing plugins together.
Add one package, claim a hostname, and test the same link on an iPhone and an Android device with the app installed and deleted.