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

Deep linking in Flutter has more moving parts than native Android or iOS: you're configuring two platforms simultaneously, and Flutter adds its own routing layer on top. This guide covers the full stack: iOS Universal Links, Android App Links, deferred deep linking, and how to avoid the pitfalls.

Flutter Deep Linking: Universal Links, App Links & Deferred Deep Links (2026 Guide)
Easy Deep links on Flutter, with Chottulink

Deep linking in Flutter has more moving parts than native Android or iOS — you're configuring two platforms simultaneously, and Flutter adds its own routing layer on top. This guide covers the full stack: iOS Universal Links, Android App Links, deferred deep linking, and how to avoid the pitfalls.


What Is Deep Linking in Flutter?

A deep link is a URL that opens your Flutter app to a specific screen rather than the home screen. In Flutter, there are two layers to handle:

  1. Platform-level configuration — telling iOS and Android that your app should handle certain URLs (Universal Links on iOS, App Links on Android)
  2. Flutter-level handling — routing the incoming URL to the correct screen in your widget tree

Deferred deep linking adds a third layer: preserving the intended destination through an app install, so new users who install via a shared link land on the right screen on first open — not the home screen.


Architecture Overview

User clicks link
     ↓
URL reaches platform (iOS / Android)
     ↓
Platform checks AASA / assetlinks.json: is there an app for this URL?
     ↓
If app is installed → OS opens app → Flutter receives URL → route to screen
     ↓
If app is NOT installed → ChottuLink stores intent → App Store/Play Store
     → install → deferred route fires on first open

Option 1: DIY Flutter Deep Linking

Flutter provides go_router or Navigator 2.0 for URL-based routing. The DIY path requires:

iOS (Universal Links):

  1. Host /.well-known/apple-app-site-association on your domain
  2. Configure Associated Domains in Xcode: applinks:yourdomain.com
  3. Handle incoming links via AppLinks or uni_links package

Android (App Links):

  1. Host /.well-known/assetlinks.json on your domain
  2. Add android:autoVerify="true" intent filter to AndroidManifest.xml

Deferred deep linking (DIY): This is where DIY gets complex. Without a platform, options are clipboard-based matching (fragile), device fingerprinting (increasingly blocked by iOS ATT), or implementing full server-side fingerprinting yourself. For most teams, a deep linking SDK is the practical path.


SDK: chottu_link: ^1.0.19 | pub.dev/packages/chottu_link
Requirements: Flutter 3.3.0+ / Dart 3.1.0+ / iOS 15.0+ / Android API 21+

With Chottulink Flutter SDK, you can wrap up the integration and be ready to create reliable deep links in less than 30 minutes.

Step 1: Install the SDK

# pubspec.yaml
dependencies:
  chottu_link: ^1.0.19
flutter pub get

Step 2: Configure Your Dashboard

In the ChottuLink dashboard:

  1. Add your iOS app (Bundle ID, App Store ID)
  2. Add your Android app (package name, SHA-256 fingerprint)
  3. Note your domain: yourapp.chottu.link (or set up a custom domain)

Step 3: Configure iOS — Associated Domains

In Xcode → your target → Signing & Capabilities+Associated Domains:

applinks:yourapp.chottu.link

If using a custom domain (link.yourapp.com), add that instead.

Step 4: Configure Android — Intent Filter

In android/app/src/main/AndroidManifest.xml, inside the <activity> block:

<!-- CRITICAL: must be false. If true, Flutter intercepts before ChottuLink. -->
<meta-data
    android:name="flutter_deeplinking_enabled"
    android:value="false"/>

<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="yourapp.chottu.link"/>
</intent-filter>

The #1 Flutter deep linking pitfall: flutter_deeplinking_enabled defaults to true, which causes Flutter to intercept incoming links before the ChottuLink SDK can process them. Deferred deep linking breaks silently. Always set this to false.

Step 5: Initialize the SDK

// main.dart
import 'package:chottu_link/chottu_link.dart';

void main() async {
  // Required: must call before runApp() when using async SDK init
  WidgetsFlutterBinding.ensureInitialized();
  await ChottuLink.init(apiKey: "your_api_key_here");
  runApp(MyApp());
}

Get your API key from: ChottuLink dashboard → Settings → API Keys.

Use either one listener based on your use case:

  • ChottuLink.onLinkReceived → Use this when you only need the resolved link.
  • ChottuLink.onLinkReceivedWithMeta → Use this when you also need metadata like isDeferred, shortLink, etc.

Do not subscribe to both for the same routing flow, or the same link can run twice.

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

class _MyAppState extends State<MyApp> {
  @override
  void initState() {
    super.initState();
    // Option 1: Use this for basic deep link handling
    ChottuLink.onLinkReceived.listen((String link) {
      _handleDeepLink(link);
    });

    // Option 2: Use this instead when you need metadata
    ChottuLink.onLinkReceivedWithMeta.listen((resolved) {
      final link = resolved.link;
      if (link == null || link.isEmpty) return;
      debugPrint("Deferred: ${resolved.isDeferred}");
      _handleDeepLink(link);
    });
  }

  void _handleDeepLink(String link) {
    final uri = Uri.parse(link);
    final path = uri.path;

    if (path.startsWith('/product/')) {
      final productId = path.split('/').last;
      Navigator.pushNamed(context, '/product', arguments: productId);
    } else if (path.startsWith('/referral/')) {
      final referralCode = uri.queryParameters['code'];
      Navigator.pushNamed(context, '/signup', arguments: referralCode);
    } else if (path.startsWith('/invite/')) {
      Navigator.pushNamed(context, '/join', arguments: uri);
    }
  }
}

Deferred deep links are delivered through the same listeners on first app open. No separate callback API is needed.

// Choose one of these (either/or):
// 1) Basic listener (no metadata)
ChottuLink.onLinkReceived.listen((link) => _handleDeepLink(link));

// 2) Metadata listener (use this if you need isDeferred/shortLink)
ChottuLink.onLinkReceivedWithMeta.listen((resolved) {
  final link = resolved.link;
  if (link == null) return;
  _handleDeepLink(link);
});

The SDK handles server-side matching: it stores the link intent when the user clicks before installing, then delivers it on first open.


Common Flutter Deep Linking Mistakes

1. flutter_deeplinking_enabled is true

Flutter's default handler intercepts Universal Links and App Links before your SDK can process them. This breaks deferred deep linking silently. Always set to false when using ChottuLink.

2. Missing WidgetsFlutterBinding.ensureInitialized()

The ChottuLink SDK init is async. Without ensureInitialized() before runApp(), the SDK may miss the initial deep link on cold start.

3. Testing on the iOS Simulator

iOS Simulators don't support Universal Links. You must test on a physical iOS device. Android emulators support App Links but can have inconsistent behavior — physical device testing is recommended for both platforms.

4. Associated Domains missing from TestFlight build

If testing via TestFlight, ensure Associated Domains are configured in both development and distribution provisioning profiles. TestFlight links break if only the development profile has the capability.

5. In-app browser interception (Instagram, WhatsApp, Telegram)

In-app browsers in social apps often intercept links and don't pass Universal Links to the OS. ChottuLink handles this with a JavaScript redirect layer that breaks out of the in-app browser context and routes to Safari or Chrome — but only when using a ChottuLink domain.


Testing Checklist

Before shipping:

Validate domain config with the ChottuLink Deep Link Tester before running the full device test.

Points to look for:

  • AASA file returns 200: https://yourapp.chottu.link/apple-app-site-association
  • assetlinks.json returns 200: https://yourapp.chottu.link/.well-known/assetlinks.json
  • flutter_deeplinking_enabled is false in AndroidManifest
  • Standard deep link: click link on physical iOS device with app installed → opens to correct screen
  • Standard deep link: click link on physical Android device → opens to correct screen
  • Deferred deep link: uninstall app → click link → install from store → open → land on correct screen (not home screen)
  • In-app browser: share a link to yourself in WhatsApp/Instagram → tap → verify correct routing
  • Cold start: kill app → tap link notification or URL → verify correct screen opens


Frequently Asked Questions

Yes. Use ChottuLink.onLinkReceived (or ChottuLink.onLinkReceivedWithMeta if you need metadata), parse the link into a Uri, then call context.go(uri.path) or GoRouter.of(context).go(uri.toString()). The SDK delivers the deep link; your router handles navigation.

Does it work with Expo?

ChottuLink Flutter SDK is for Flutter. For Expo React Native projects, use the React Native SDK with expo-dev-client. The Flutter SDK doesn't apply to Expo.

Does it work on Flutter Web?

ChottuLink deep linking is for mobile apps (iOS and Android). Flutter Web doesn't support deferred deep linking. Standard URL routing on Flutter Web doesn't require a deep linking SDK.

What's the minimum Dart SDK version?

For the latest SDK (v1.0.18+), minimum is Dart 3.1.0+.
For older SDKs (v1.0.11 to v1.0.17), minimum is Dart 3.5.0+.

Does deferred deep linking work with iOS ATT (App Tracking Transparency)?

Yes. ChottuLink's deferred deep linking uses server-side matching, not device fingerprinting or IDFA. No ATT permission is required.

Yes, on the Growth plan ($39/mo) and above. Set up link.yourapp.com in the dashboard and update your Associated Domains and intent filter to match.


Flutter SDK docs at docs.chottulink.com →

Get Started →