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
User taps a Redirectly link (e.g., yourapp.redirectly.app/product/123) in a browser, email, or social media.
Redirectly servers capture the click, the URL parameters, UTM data, device info, and geo location. This becomes the "deferred link data."
If the app isn't installed, the user is redirected to the App Store or Google Play (or a custom landing page).
The user installs and launches your app for the first time.
Your flutter_redirectly SDK makes a single API call to Redirectly to fetch the stored link data for this new install.
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 & 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 --versionto 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.
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.Go to the AASA Validator Tool.
- 2.Enter your subdomain (e.g., yourapp.redirectly.app).
- 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."
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.Go to the assetlinks.json Validator Tool.
- 2.Enter your subdomain (e.g., yourapp.redirectly.app).
- 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.
SDK Installation & Initialization
Step 3.1: Add flutter_redirectly to pubspec.yaml
pubspec.yaml
dependencies:
flutter:
sdk: flutter
flutter_redirectly: ^2.1.6Then 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
});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!'),
);
},
),
);
}
}Handling Deferred Deep Links
Deferred deep links are the crown jewel of Redirectly. When a user clicks a link before installing your app, the SDK retrieves the deferred data after the first app launch and routes them to the intended screen.
How Deferred Deep Links Work (Technical Deep Dive)
- 1. Link Click
User taps a Redirectly link (e.g., yourapp.redirectly.app/product/123?utm_source=instagram) in Safari, an email, or social media.
- 2. Server-Side Capture
Redirectly's servers capture: the URL, all query parameters, the user's IP address, User-Agent, geo location, and device info. This is stored as a "pending link".
- 3. Redirect to Store
If the app isn't installed, the user is redirected to the App Store (iOS) or Google Play (Android), or a custom landing page.
- 4. App Install & Launch
The user installs the app from the store and launches it for the first time.
- 5. SDK Fetches Deferred Data
Your flutter_redirectly SDK automatically makes an API call to Redirectly with the device's identifiers (advertising ID, etc.) to retrieve the stored link data.
- 6. Match & Route
Redirectly matches the device and returns the deferred link. Your app parses the URL and routes the user to the exact screen they intended.
Step 5.1: Retrieve Deferred Link Data
The SDK automatically handles most of this, but you need to listen for the deferred link and act on it:
lib/main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize Redirectly
final redirectly = Redirectly.instance;
await redirectly.initialize(
RedirectlyConfig(
apiKey: 'your-api-key',
subdomain: 'yourapp.redirectly.app',
),
);
// Listen for deferred deep link
redirectly.onAppInstalled.listen((link) {
print('Deferred link data:');
print(' URL: ${link.url}');
print(' Path: ${link.deepLinkPath}');
print(' Parameters: ${link.parameters}');
print(' UTM Source: ${link.parameters['utm_source']}');
// Navigate to the deferred content
// This will be handled by go_router or your navigation logic
});
runApp(const MyApp());
}Step 5.2: Access Attribution Data
The RedirectlyLink object contains all the data you need for attribution:
final link = await redirectly.getDeferredLink();
if (link != null) {
// Original URL that was clicked
final originalUrl = link.url; // e.g., yourapp.redirectly.app/product/123?utm_source=instagram
// Extracted path for routing
final deepLinkPath = link.deepLinkPath; // e.g., /product/123
// All query parameters and UTM data
final parameters = link.parameters;
final utmSource = parameters['utm_source']; // instagram
final utmCampaign = parameters['utm_campaign'];
// Device and geo info
final deviceId = link.deviceId;
final geoCountry = link.geoCountry;
final geoCity = link.geoCity;
// Log this to your analytics
analytics.logDeferredLinkReceived(
url: originalUrl,
utmSource: utmSource,
utmCampaign: utmCampaign,
);
}Step 5.3: Integration with go_router
Update your go_router redirect logic to handle deferred links:
FutureOr<String?> _handleRedirect(
BuildContext context,
GoRouterState state,
) async {
final redirectly = Redirectly.instance;
// Check for deferred deep link on first app launch
final deferredLink = await redirectly.getDeferredLink();
if (deferredLink != null) {
print('Routing to deferred link: ${deferredLink.deepLinkPath}');
// Optional: Store attribution data
_storeAttributionData(deferredLink);
// Return the path to navigate to
return deferredLink.deepLinkPath;
}
// Otherwise, check for immediate deep link
final immediateLink = await redirectly.getLink();
if (immediateLink != null) {
return immediateLink.deepLinkPath;
}
return null;
}
void _storeAttributionData(RedirectlyLink link) {
// Store the attribution data for later analysis
final prefs = SharedPreferences.getInstance();
prefs.then((p) {
p.setString('install_source', link.parameters['utm_source'] ?? 'direct');
p.setString('install_campaign', link.parameters['utm_campaign'] ?? '');
});
}Pro Tip: Attribution Tracking
Always store the utm_source, utm_campaign, and other attribution data from deferred links. This is crucial for understanding which marketing channels drive installations and conversions.
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.Log in to your Redirectly dashboard.
- 2.Create a test link (e.g., yourapp.redirectly.app/product/123).
- 3.Click the link on your device and observe the analytics update in real-time.
- 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 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
| Feature | Firebase Dynamic Links | Redirectly |
|---|---|---|
| SDK Type | Firebase (platform-specific) | Pure Dart |
| Native Code Required | Yes (Kotlin/Swift) | No (config only) |
| Deferred Deep Linking | Yes | Yes |
| Custom Domain | firebaseapp.com only | Custom domain support |
| Install Attribution | Basic (Firebase Analytics) | Real-time dashboard |
| Maintenance | Deprecated (no updates) | Active support |
Migration Steps
- 1. Remove Firebase
Remove the firebase_dynamic_links package from pubspec.yaml and all Firebase initialization code.
- 2. Add Redirectly
Follow Step 3 of this guide to add flutter_redirectly to your project.
- 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. 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. 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. 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. 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.
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:
Create Your Account
Sign up for free at redirectly.app and configure your custom subdomain.
Get started →Follow This Guide
Implement the 6 steps in order: iOS, Android, SDK, go_router, deferred links, testing.
Start with Step 1 →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.