Flutter

Flutter Deep Linking Tutorial

Master deferred deep linking, iOS Universal Links, Android App Links, and install attribution. Complete step-by-step guide with code examples for flutter_redirectly SDK.

Introduction

Why Deep Linking Matters for Your Flutter App

Deep linking is the cornerstone of modern mobile app growth. When a user clicks a link to specific content in your app, they should land directly on that screen—whether the app is already installed or not. This seamless experience is critical for user retention, engagement, and understanding which marketing campaigns drive installs.

Traditional deep links work fine if the app is installed: the OS routes the link directly to your app, and you parse the URL to navigate to the right screen. But what happens when a user hasn't installed your app yet? They click a marketing link, get redirected to the App Store or Google Play, install the app, and open it—only to land on the home screen, losing all context about what they originally intended to view. This is where deferred deep linking changes the game.

How Deferred Deep Linking Works

1. User clicks link

User taps a Redirectly link (e.g., yourapp.redirectly.app/product/123) in a browser, email, or social media.

2. Redirectly captures intent

Redirectly servers capture the click, the URL parameters, UTM data, device info, and geo location. This becomes the "deferred link data."

3. App not installed?

If the app isn't installed, the user is redirected to the App Store or Google Play (or a custom landing page).

4. User installs & opens app

The user installs and launches your app for the first time.

5. SDK retrieves deferred data

Your flutter_redirectly SDK makes a single API call to Redirectly to fetch the stored link data for this new install.

6. Route user correctly

Your app routes the user to the exact screen they intended (e.g., product/123) with full context restored.

Deferred Deep Linking

Links work before AND after app install. Users always land on the right screen, even if the app wasn't installed when they clicked.

Install Attribution

Understand which marketing campaigns, channels, and links drive app installs. Track conversions from first click to purchase.

Pure Dart SDK

No native Kotlin or Swift code required. One line of initialization in main.dart and you're ready.

iOS Universal Links

Secure, native deep linking on iOS via apple-app-site-association. No Safari redirects or user prompts.

Android App Links

Verified deep linking on Android via assetlinks.json. Direct app opening without browser fallback.

Prerequisites

Prerequisites & Requirements

Before you begin implementing deferred deep linking in your Flutter app, ensure you have the following in place:

  • Flutter 3.0+

    flutter_redirectly requires Flutter 3.0 or later. Run flutter --version to check.

  • Dart 3.0+

    The SDK uses modern Dart null-safety features. Your project must target Dart 3.0 or later.

  • Redirectly Account

    Sign up for free at redirectly.app and create an API key.

  • Custom Subdomain

    Configure your custom subdomain (e.g., yourapp.redirectly.app) in your Redirectly dashboard. This will be used for all deep links.

  • iOS Development Environment

    Xcode 14+, an Apple Developer account, and a provisioning profile for your app.

  • Android Development Environment

    Android Studio, SDK Platform 21+, and a registered app in your Google Play Console.

Step 1: iOS

iOS Universal Links Setup

iOS uses Universal Links to route web URLs to your app. Universal Links are cryptographically verified associations between your domain and your app, defined by a file called apple-app-site-association (AASA) on your server. Redirectly hosts this file for you, so you don't need to set up your own server.

Step 1.1: Enable Associated Domains in Xcode

First, enable the Associated Domains capability in your iOS project:

  • 1.Open ios/Runner.xcworkspace in Xcode (not Runner.xcodeproj).
  • 2.Select the Runner target.
  • 3.Go to Signing & Capabilities tab.
  • 4.Click + Capability and search for "Associated Domains".
  • 5.Click to add the capability to your target.

Step 1.2: Add Your Domain to Runner.entitlements

After enabling the capability, Xcode creates an entitlements file. You need to add your Redirectly subdomain:

ios/Runner/Runner.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.developer.associated-domains</key>
	<array>
		<string>applinks:yourapp.redirectly.app</string>
		<string>webcredentials:yourapp.redirectly.app</string>
	</array>
</dict>
</plist>

Replace yourapp.redirectly.app with your actual Redirectly subdomain configured in the dashboard.

Step 1.3: Verify AASA File Configuration

Redirectly automatically hosts the apple-app-site-association file for your subdomain. To verify it's correctly configured:

  1. 1.Go to the AASA Validator Tool.
  2. 2.Enter your subdomain (e.g., yourapp.redirectly.app).
  3. 3.The tool will fetch and validate the AASA file. You should see your app ID listed in the apps section.

HTTPS Required

Universal Links only work with HTTPS domains. Redirectly uses HTTPS for all subdomains, so this is handled automatically.

Step 1.4: What the AASA File Contains

Here's an example of what Redirectly serves for your AASA configuration (you don't need to create this—Redirectly does it automatically):

Example AASA File

{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "TEAMID.com.yourcompany.yourapp",
        "paths": ["/*"]
      }
    ]
  }
}

The AASA file tells iOS: "Any URL matching yourapp.redirectly.app/* should be opened with the app ID TEAMID.com.yourcompany.yourapp, not Safari."

Step 2: Android

Android App Links Setup

Android uses App Links for verified deep linking. App Links require an intent filter in your AndroidManifest.xml and a cryptographically verified JSON file (assetlinks.json) served from your domain. Like AASA, Redirectly hosts assetlinks.json for you.

Step 2.1: Add Intent Filter to AndroidManifest.xml

Add an intent filter to the activity that handles deep links (usually MainActivity). The autoVerify="true" attribute tells Android to verify the domain:

android/app/src/main/AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
  <application>
    <activity
      android:name=".MainActivity"
      android:exported="true">

      <!-- Normal launch intent filter -->
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>

      <!-- App Links intent filter for deep linking -->
      <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.redirectly.app" />
      </intent-filter>

    </activity>
  </application>
</manifest>

Replace yourapp.redirectly.app with your actual Redirectly subdomain. The android:exported="true" is required for activities that can be launched by other apps.

Step 2.2: Verify assetlinks.json Configuration

Redirectly automatically hosts the assetlinks.json file. Verify it's correctly configured:

  1. 1.Go to the assetlinks.json Validator Tool.
  2. 2.Enter your subdomain (e.g., yourapp.redirectly.app).
  3. 3.The tool will fetch and validate the file. You should see your package name and SHA256 fingerprint listed.

Step 2.3: What the assetlinks.json File Contains

Here's an example of what Redirectly serves (you don't need to create this):

Example assetlinks.json

[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.yourcompany.yourapp",
      "sha256_cert_fingerprints": [
        "AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99"
      ]
    }
  }
]

The assetlinks.json file tells Android: "The app with package name com.yourcompany.yourapp and this SHA256 certificate is allowed to handle URLs for yourapp.redirectly.app."

Certificate Fingerprint Must Match

The SHA256 certificate fingerprint in assetlinks.json must match the certificate you use to sign your APK/AAB. If you release a new build signed with a different key, you'll need to update the fingerprint.

Step 3: SDK

SDK Installation & Initialization

Step 3.1: Add flutter_redirectly to pubspec.yaml

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  flutter_redirectly: ^2.1.6

Then run flutter pub get to install the package.

Step 3.2: Initialize Redirectly in main.dart

Initialize Redirectly as early as possible in your app's lifecycle, ideally in main() or before your app runs:

lib/main.dart

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

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

  // Initialize Redirectly
  final redirectly = Redirectly.instance;
  await redirectly.initialize(
    RedirectlyConfig(
      apiKey: 'your-api-key',
      subdomain: 'yourapp.redirectly.app',
      debug: true, // Set to false in production
    ),
  );

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Your App',
      home: const HomePage(),
    );
  }
}

Replace your-api-key with your API key from the Redirectly dashboard, and yourapp.redirectly.app with your actual subdomain.

Step 3.3: Configure Redirect Handling

After initialization, configure how your app handles incoming deep links:

lib/main.dart (continued)

// Listen for incoming deep links
redirectly.linkStream.listen((link) {
  print('Received deep link: ${link.url}');
  print('Parameters: ${link.parameters}');
  // Handle the link and navigate in your app
});

// Listen for deferred deep links (new install)
redirectly.onAppInstalled.listen((link) {
  print('App just installed from link: ${link.url}');
  // Navigate to the deferred content
});
Step 4: go_router

go_router Integration

go_router is the modern routing solution for Flutter apps. It provides declarative routing with deep link support. Integrating Redirectly with go_router is straightforward.

Step 4.1: Configure go_router with Deep Link Routes

First, add go_router to your pubspec.yaml:

dependencies:
  go_router: ^13.0.0

Step 4.2: Set Up go_router with Redirectly

lib/router.dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_redirectly/flutter_redirectly.dart';

final goRouter = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      name: 'home',
      builder: (context, state) => const HomePage(),
      routes: [
        GoRoute(
          path: 'product/:id',
          name: 'product',
          builder: (context, state) {
            final productId = state.pathParameters['id'];
            return ProductDetailPage(productId: productId ?? '');
          },
        ),
        GoRoute(
          path: 'user/:userId',
          name: 'user',
          builder: (context, state) {
            final userId = state.pathParameters['userId'];
            return UserProfilePage(userId: userId ?? '');
          },
        ),
      ],
    ),
  ],
  redirect: _handleRedirect,
);

// Handle incoming deep links
FutureOr<String?> _handleRedirect(
  BuildContext context,
  GoRouterState state,
) async {
  final redirectly = Redirectly.instance;

  // Check for deferred deep link
  final deferredLink = await redirectly.getDeferredLink();
  if (deferredLink != null) {
    return deferredLink.deepLinkPath;
  }

  return null;
}

Step 4.3: Use go_router in Your App

lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter_redirectly/flutter_redirectly.dart';
import 'router.dart';

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

  // Initialize Redirectly
  final redirectly = Redirectly.instance;
  await redirectly.initialize(
    RedirectlyConfig(
      apiKey: 'your-api-key',
      subdomain: 'yourapp.redirectly.app',
    ),
  );

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Your App',
      routerConfig: goRouter,
    );
  }
}

Step 4.4: Handle Link Events with StreamBuilder

For real-time handling of deep links when the app is already running, use a StreamBuilder:

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final redirectly = Redirectly.instance;

    return Scaffold(
      appBar: AppBar(title: const Text('Home')),
      body: StreamBuilder<RedirectlyLink>(
        stream: redirectly.linkStream,
        builder: (context, snapshot) {
          if (snapshot.hasData) {
            final link = snapshot.data!;
            // Handle the deep link
            WidgetsBinding.instance.addPostFrameCallback((_) {
              context.go(link.deepLinkPath);
            });
          }

          return const Center(
            child: Text('Welcome to your app!'),
          );
        },
      ),
    );
  }
}
Step 6: Testing

Testing & Debugging

Testing deep links is crucial before launching your app. Here's how to test on both platforms.

iOS Simulator Testing

iOS simulators have limitations with Universal Links. Use the xcrun simctl command to simulate link clicks:

Terminal

# Get your simulator UDID
xcrun simctl list devices

# Open a deep link in the simulator
xcrun simctl openurl booted https://yourapp.redirectly.app/product/123

# Simulate app install + open with deferred data
xcrun simctl openurl booted https://yourapp.redirectly.app/product/456?utm_source=instagram

The booted argument targets the currently running simulator. For a specific simulator, use its UDID.

Android Emulator Testing

Android emulators fully support App Links testing. Use adb shell:

Terminal

# List connected emulators/devices
adb devices

# Start an app with a deep link intent
adb shell am start -a android.intent.action.VIEW \
  -d "https://yourapp.redirectly.app/product/123" \
  com.yourcompany.yourapp

# Test with query parameters
adb shell am start -a android.intent.action.VIEW \
  -d "https://yourapp.redirectly.app/product/456?utm_source=google" \
  com.yourcompany.yourapp

Real Device Testing

For comprehensive testing, use real devices:

  • iOS Real Device:

    Build and run on a real iPhone. Open links in Safari or any app (Mail, Messages, Twitter, etc.). Universal Links will open your app.

  • Android Real Device:

    Build and run on a real Android phone. App Links will automatically open your app when you click the link.

Verify with Redirectly Dashboard

The Redirectly dashboard shows real-time analytics for every link you create:

  1. 1.Log in to your Redirectly dashboard.
  2. 2.Create a test link (e.g., yourapp.redirectly.app/product/123).
  3. 3.Click the link on your device and observe the analytics update in real-time.
  4. 4.Check logs to see if your app received the link data correctly.

Common Issues & Troubleshooting

Link opens in browser instead of app

Cause: AASA (iOS) or assetlinks.json (Android) is not properly configured or served.

Fix: Use the AASA Validator and assetlinks Validator to verify your configuration.

Deferred link data not received

Cause: SDK not initialized before calling getDeferredLink(), or API key is invalid.

Fix: Ensure Redirectly.initialize() is called in main() before your app runs. Check that your API key is correct.

App crashes on deep link

Cause: Path parameters are null or route doesn't match the deep link path.

Fix: Add null-safety checks for path parameters in your route builders. Log the incoming link path and verify it matches your go_router routes.

iOS Universal Links not working

Cause: Associated Domains capability not enabled, or applinks: prefix is missing.

Fix: Re-check Step 1.1 and 1.2. Make sure you added the capability in Xcode and the applinks: prefix is in your entitlements file.

Android App Links not working

Cause: Intent filter missing android:autoVerify="true", or certificate fingerprint doesn't match.

Fix: Verify your AndroidManifest.xml has autoVerify=true. Get your APK's SHA256 fingerprint using keytool or Android Studio and ensure it's in assetlinks.json.

Testing Checklist

  • ✓ AASA file validates successfully
  • ✓ assetlinks.json file validates successfully
  • ✓ Deep link opens app (not browser) on iOS device
  • ✓ Deep link opens app (not browser) on Android device
  • ✓ App routes to correct screen after link click
  • ✓ Deferred link works after fresh install
  • ✓ UTM parameters and query data are captured
  • ✓ Redirectly dashboard shows link clicks and conversions
Migration

Migration from Firebase Dynamic Links

Firebase Dynamic Links (FDL) was deprecated in March 2024. If your app uses FDL, migrating to Redirectly is straightforward. The flow is similar, but with a pure Dart SDK and better performance.

Quick Comparison

FeatureFirebase Dynamic LinksRedirectly
SDK TypeFirebase (platform-specific)Pure Dart
Native Code RequiredYes (Kotlin/Swift)No (config only)
Deferred Deep LinkingYesYes
Custom Domainfirebaseapp.com onlyCustom domain support
Install AttributionBasic (Firebase Analytics)Real-time dashboard
MaintenanceDeprecated (no updates)Active support

Migration Steps

  1. 1. Remove Firebase

    Remove the firebase_dynamic_links package from pubspec.yaml and all Firebase initialization code.

  2. 2. Add Redirectly

    Follow Step 3 of this guide to add flutter_redirectly to your project.

  3. 3. Update Configuration

    Replace Firebase initialization with Redirectly initialization (see Step 3.2).

    Firebase: Used google-services.json and Firebase Console.

    Redirectly: Simple API key + subdomain configuration.

  4. 4. Update Link Handling

    Replace Firebase Dynamic Links listeners with Redirectly listeners.

    Old Firebase code:

    FirebaseDynamicLinks.instance.onLink.listen((dynamicLink) {
      // Handle the link
    });

    New Redirectly code:

    final redirectly = Redirectly.instance;
    redirectly.linkStream.listen((link) {
      // Handle the link
    });
  5. 5. Update Platform Config

    Follow Steps 1 and 2 of this guide to set up iOS Universal Links and Android App Links for your Redirectly subdomain.

  6. 6. Update Link Generation

    Update anywhere you generate Firebase Dynamic Links to use Redirectly links instead.

    Firebase: Used DynamicLinkParameters in code.

    Redirectly: Simply format your deep link URLs (e.g., yourapp.redirectly.app/product/123?utm_source=email).

  7. 7. Test & Deploy

    Follow Step 6 of this guide to test deep links on iOS and Android before releasing your app update.

For a detailed code comparison and step-by-step walkthrough, see our Firebase Dynamic Links migration guide.

Also check out our Firebase Dynamic Links alternative page for more information on why Redirectly is a better choice.

FAQ

Frequently Asked Questions

Does Redirectly require native code for Flutter?

No, flutter_redirectly is a pure Dart SDK. You don't need to write any Kotlin or Swift code. The only native configuration required is:

  • • iOS: Add applinks entry to Runner.entitlements (XML config)
  • • Android: Add intent filter to AndroidManifest.xml (XML config)

These are simple configuration files, not code.

Does Redirectly work with go_router?

Yes, Redirectly integrates seamlessly with go_router. See Step 4 for a complete example showing how to configure go_router with Redirectly deep link handling, including deferred link support in the redirect callback.

How does deferred deep linking work?

When a user clicks a Redirectly link before installing the app, Redirectly's servers capture the URL and all parameters. After the user installs and opens the app, the flutter_redirectly SDK retrieves this captured data and passes it to your app, allowing you to route the user to the intended screen. See the "How Deferred Deep Linking Works" section for technical details.

Can I test deep links on iOS simulator?

iOS simulators have limitations with Universal Links, but you can simulate link clicks using the xcrun simctl command:

xcrun simctl openurl booted https://yourapp.redirectly.app/product/123

For comprehensive testing, use a real iOS device. See Step 6 for more details.

How is Redirectly different from Firebase Dynamic Links?

Firebase Dynamic Links was deprecated in March 2024. Redirectly is a modern alternative with a pure Dart SDK (no native code required), better performance, real-time analytics dashboard, active support, and custom domain support. See the migration guide for a detailed comparison.

Does Redirectly support custom domains?

Yes, you can use your own domain (e.g., links.yourcompany.com) instead of the default redirectly.app domain. Configure it in your Redirectly account settings and add the appropriate DNS records. The setup is the same—your domain will host the AASA and assetlinks.json files automatically.

What happens if AASA or assetlinks.json is misconfigured?

If these files are missing or incorrect, the OS will not recognize your domain as a verified app domain. When users click your links, they'll open in the browser instead of your app. Use our validation tools (AASA Validator and assetlinks Validator) to verify your configuration.

Can I use Redirectly with other deep linking solutions?

Yes, Redirectly can work alongside other solutions. However, if you're using Firebase Dynamic Links, we recommend fully migrating to Redirectly (the FDL API is deprecated anyway). For custom deep link schemes, Redirectly's Universal Links and App Links approach is more reliable and doesn't conflict with other solutions.

Next Steps

You're now equipped with everything you need to implement deferred deep linking in your Flutter app. Here's what to do next:

1

Create Your Account

Sign up for free at redirectly.app and configure your custom subdomain.

Get started →
2

Follow This Guide

Implement the 6 steps in order: iOS, Android, SDK, go_router, deferred links, testing.

Start with Step 1 →
3

Read the Docs

Deep dive into the Redirectly SDK documentation for advanced features.

View Flutter docs →

Questions or issues? Check out our general deep linking guide, React Native deep linking guide, or reach out to our support team.

Get started with Flutter deep linking

Sign up for free and get your custom subdomain. Follow this guide to implement deferred deep linking in minutes.