Adding Deep Linking Support
Deep links direct users to specific content within your app. When users tap a deep link on a device with your app installed, the app opens directly to the intended content, such as a product page or specific experience.
Singular tracking links support both standard deep linking (for installed apps) and deferred deep linking (for new installs). For comprehensive information, see the Deep Linking FAQ and Singular Links FAQ.
Requirements
Prerequisites
Complete the Singular Links Prerequisites to enable deep linking for your app.
Notes:
- This article assumes your organization is using Singular Links - Singular's tracking link technology. Older customers may be using legacy tracking links.
- Your app's deep link destinations need to be configured on the Apps page in Singular (see Configuring Your App for Attribution Tracking).
Available Parameters
The SingularLink handler provides access to deep link, deferred deep link, and passthrough parameters from Singular tracking links when the app opens.
- Deep Link (_dl): The destination URL within your app for users clicking the link
- Deferred Deep Link (_ddl): The destination URL for users who install the app after clicking the link
- Passthrough (_p): Custom data passed through the tracking link for additional context
Flutter Version Compatibility
Flutter 3.35 Breaking Change
Flutter 3.35 introduced a breaking change in how deep links are processed.
The internal didPushRouteInformation() function now intercepts
and decodes deep links before third-party SDKs can handle them. This creates
a conflict with Singular's deep link handler, causing Singular links to be
treated as unknown routes and routing users to the home screen instead of
the intended destination.
This affects both Android and iOS. If your app targets Flutter 3.35 or later, complete the additional platform-specific configuration below before proceeding with the standard deep link implementation.
Note: This is a Flutter framework-level change, not a Singular SDK bug. No SDK update is required. The configurations below restore correct deep link routing behavior for all app states (foreground, background, and cold start).
Android Configuration (Flutter 3.35+)
Two changes are required on Android: disabling Flutter's built-in deep link handler and manually forwarding intents to both the Flutter engine and the Singular SDK.
Step 1: Disable Flutter's Deep Link Handler
In your AndroidManifest.xml, add the following
<meta-data> entry inside the <activity> tag:
<meta-data
android:name="flutter_deeplinking_enabled"
android:value="false" />
Step 2: Manually Forward Intents
Update your MainActivity to forward intents to both the
Flutter engine and the Singular SDK.
// Add as part of the imports at the top of the class
import android.content.Intent
import com.singular.flutter_sdk.SingularBridge
// Add to the MainActivity class
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
// Forward to Flutter engine
flutterEngine?.activityControlSurface?.onNewIntent(intent)
// Forward to Singular SDK
if (intent.data != null) {
SingularBridge.onNewIntent(intent)
}
}
// Add as part of the imports at the top of the class
import android.content.Intent;
import com.singular.flutter_sdk.SingularBridge;
// Add to the MainActivity class
@Override
public void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
// Forward to Flutter engine
if (getFlutterEngine() != null) {
getFlutterEngine().getActivityControlSurface().onNewIntent(intent);
}
// Forward to Singular SDK
if (intent.getData() != null) {
SingularBridge.onNewIntent(intent);
}
}
Note: This replaces the standard
onNewIntent implementation shown in the
Android Platform Configuration section below for
Flutter 3.35+ projects. If you are on Flutter 3.10.x or earlier, use
the standard implementation instead.
iOS Configuration (Flutter 3.35+)
In your app's Info.plist, set the
FlutterDeepLinkingEnabled flag to false to
disable Flutter's native deep link interception:
<key>FlutterDeepLinkingEnabled</key>
<false/>
Note: No additional changes to
AppDelegate are required beyond this flag. Continue with
the standard iOS AppDelegate configuration shown in the
iOS Platform Configuration section below.
Platform Configuration
iOS Platform Configuration
Update iOS AppDelegate
Enable the Singular SDK to process launch-related data and handle deep
links by passing the launchOptions and
userActivity objects to the Singular SDK in your
AppDelegate file.
These objects contain critical information about how and why your app was launched, which Singular uses for attribution tracking and deep link navigation.
Objective-C Implementation
// Top of AppDelegate.m
#import "SingularAppDelegate.h"
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[GeneratedPluginRegistrant registerWithRegistry:self];
[SingularAppDelegate shared].launchOptions = launchOptions;
return [super application:application
didFinishLaunchingWithOptions:launchOptions];
}
- (BOOL)application:(UIApplication *)application
continueUserActivity:(NSUserActivity *)userActivity
restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>>
*restorableObjects))restorationHandler {
[[SingularAppDelegate shared] continueUserActivity:userActivity
restorationHandler:restorationHandler];
return [super application:application
continueUserActivity:userActivity
restorationHandler:restorationHandler];
}
- (BOOL)application:(UIApplication *)app
openURL:(NSURL *)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
[[SingularAppDelegate shared] handleOpenUrl:url options:options];
return [super application:app openURL:url options:options];
}
Swift Implementation
import singular_flutter_sdk
override func application(_ application: UIApplication,
didFinishLaunchingWithOptions
launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
GeneratedPluginRegistrant.register(with: self)
if let singularAppDelegate = SingularAppDelegate.shared() {
singularAppDelegate.launchOptions = launchOptions
}
return super.application(application,
didFinishLaunchingWithOptions:launchOptions)
}
override func application(_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
if let singularAppDelegate = SingularAppDelegate.shared() {
singularAppDelegate.continueUserActivity(userActivity,
restorationHandler: nil)
}
return super.application(application,
continue: userActivity,
restorationHandler: restorationHandler)
}
override func application(_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
if let singularAppDelegate = SingularAppDelegate.shared() {
singularAppDelegate.handleOpen(url, options: options)
}
return super.application(app, open: url, options: options)
}
Android Platform Configuration
Update Android MainActivity
Enable the Singular SDK to process launch-related data and handle deep
links by modifying the MainActivity file to pass the
Intent object to the Singular SDK.
The Intent object contains information about how and why
your app was launched, which Singular uses for attribution tracking and
deep link navigation.
Java Implementation
// Add as part of the imports at the top of the class
import android.content.Intent;
import com.singular.flutter_sdk.SingularBridge;
// Add to the MainActivity class
@Override
public void onNewIntent(Intent intent) {
if(intent.getData() != null) {
setIntent(intent);
super.onNewIntent(intent);
SingularBridge.onNewIntent(intent);
}
}
Kotlin Implementation
// Add as part of the imports at the top of the class
import android.content.Intent
import com.singular.flutter_sdk.SingularBridge
// Add to the MainActivity class
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
if (intent.data != null) {
setIntent(intent)
SingularBridge.onNewIntent(intent)
}
}
SDK Configuration
Implement Singular Links Handler
Configure the singularLinksHandler callback during SDK initialization
to handle incoming deep link and deferred deep link data.
import 'package:flutter/material.dart';
import 'package:singular_flutter_sdk/singular.dart';
import 'package:singular_flutter_sdk/singular_config.dart';
import 'package:singular_flutter_sdk/singular_link_params.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
initializeSDK();
}
void initializeSDK() {
// Create SDK configuration
SingularConfig config = SingularConfig(
'YOUR_SDK_KEY',
'YOUR_SDK_SECRET'
);
// Configure deep link handler
config.singularLinksHandler = (SingularLinkParams params) {
print('=== Singular Link Resolved ===');
print('Deep link: ${params.deeplink}');
print('Passthrough: ${params.passthrough}');
print('Is deferred: ${params.isDeferred}');
// Handle deep link navigation
if (params.deeplink != null) {
handleDeepLink(params.deeplink!, params.isDeferred);
}
// Handle passthrough data
if (params.passthrough != null) {
handlePassthroughData(params.passthrough!);
}
};
// Initialize SDK
Singular.start(config);
}
void handleDeepLink(String url, bool isDeferred) {
print('Routing to: $url (Deferred: $isDeferred)');
// Parse URL and navigate to appropriate screen
// Example: myapp://product/123
if (url.contains('product')) {
final productId = url.split('/').last;
navigateToProduct(productId);
} else if (url.contains('promo')) {
navigateToPromo(url);
}
}
void handlePassthroughData(String passthrough) {
print('Processing passthrough data: $passthrough');
// Parse and use passthrough data as needed
}
void navigateToProduct(String productId) {
// Your navigation logic
print('Navigating to product: $productId');
}
void navigateToPromo(String url) {
// Your navigation logic
print('Navigating to promo: $url');
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(),
);
}
}
Configuration Property:
void Function(SingularLinkParams)? singularLinksHandler
Note: The singularLinksHandler callback
is triggered only when the app opens through a Singular Link. For
more information, see the
Singular Links FAQ.
For complete documentation, see singularLinksHandler reference.
Handler Behavior
Understanding Link Resolution
The singularLinksHandler behaves differently depending on whether the app is freshly installed or already installed.
Fresh Install (Deferred Deep Link)
On a fresh install, no Open URL exists when the app launches. Singular completes attribution to determine if the tracking link contained a deep link or deferred deep link value.
Deferred Deep Link Flow:
- User clicks a Singular tracking link configured with a deep link value
- User installs and opens the app for the first time
- Singular SDK sends the first session to Singular servers
- Attribution completes and identifies the deep link from the tracking link
- Deep link value returns to the
singularLinksHandlercallback in thedeeplinkparameter withisDeferred = true
Testing Deferred Deep Links:
- Uninstall the app from the test device (if currently installed)
- iOS: Reset your IDFA. Android: Reset your Google Advertising ID (GAID)
- Click the Singular tracking link from the device (ensure it's configured with a deep link value)
- Install and open the app
Attribution should complete successfully, and the deferred deep link value will be passed to the singularLinksHandler handler.
Pro Tip: When testing deep links with a development build using a different package name (e.g., com.example.dev instead of com.example.prod), configure the tracking link specifically for the development app's package name. After clicking the test link, install the development build directly onto the device (via Android Studio or Xcode) rather than downloading the production app from the app store.
Already Installed (Immediate Deep Link)
When the app is already installed, clicking a Singular Link opens the app immediately using Universal Links (iOS) or Android App Links technology.
Immediate Deep Link Flow:
- User clicks a Singular tracking link
- The operating system provides an Open URL containing the entire Singular tracking link
- During SDK initialization, Singular parses the URL
- Singular extracts the
deeplinkandpassthroughvalues - Values return through the
singularLinksHandlerhandler withisDeferred = false
Advanced Features
Passthrough Parameters
Capture additional data from the tracking link click using passthrough parameters.
If a passthrough (_p) parameter is included in the tracking
link, the singularLinksHandler handler's
passthrough parameter contains the corresponding data. Use
this for capturing campaign metadata, user segmentation data, or any
custom information you need in the app.
import 'package:singular_flutter_sdk/singular_config.dart';
import 'package:singular_flutter_sdk/singular_link_params.dart';
import 'dart:convert';
SingularConfig config = SingularConfig('API_KEY', 'SECRET');
config.singularLinksHandler = (SingularLinkParams params) {
// Extract passthrough data
final passthroughData = params.passthrough;
if (passthroughData != null) {
try {
// Parse JSON passthrough data
final jsonData = jsonDecode(passthroughData);
print('Campaign ID: ${jsonData['campaign_id']}');
print('User Segment: ${jsonData['segment']}');
print('Promo Code: ${jsonData['promo_code']}');
// Apply campaign-specific settings
applyCampaignSettings(jsonData);
} catch (error) {
print('Error parsing passthrough data: $error');
}
}
};
void applyCampaignSettings(Map<String, dynamic> data) {
// Your campaign logic here
print('Applying campaign settings: $data');
}
Forward All Query Parameters
Capture all query parameters from the tracking link URL by appending the _forward_params=2 parameter to your tracking link.
When _forward_params=2 is added to the tracking link, all query parameters are included in the deeplink parameter of the singularLinksHandler handler, giving you access to the complete URL with all its parameters.
Example Tracking Link:https://yourapp.sng.link/A1b2c/abc123?_dl=myapp://product/123&_forward_params=2&utm_source=facebook&promo=SALE2024
The singularLinksHandler handler will receive:deeplink = "myapp://product/123?utm_source=facebook&promo=SALE2024"
Complete Implementation Example
Comprehensive deep linking implementation with navigation, passthrough handling, and parameter forwarding for Flutter apps.
import 'package:flutter/material.dart';
import 'package:singular_flutter_sdk/singular.dart';
import 'package:singular_flutter_sdk/singular_config.dart';
import 'package:singular_flutter_sdk/singular_link_params.dart';
import 'dart:convert';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
@override
void initState() {
super.initState();
initializeSDK();
}
void initializeSDK() {
SingularConfig config = SingularConfig(
'YOUR_SDK_KEY',
'YOUR_SDK_SECRET'
);
config.singularLinksHandler = (SingularLinkParams params) {
print('=== Singular Link Resolved ===');
print('Deep link: ${params.deeplink}');
print('Passthrough: ${params.passthrough}');
print('Is deferred: ${params.isDeferred}');
// Handle passthrough data
if (params.passthrough != null) {
handlePassthroughData(params.passthrough!);
}
// Handle deep link navigation
if (params.deeplink != null) {
handleDeepLinkNavigation(params.deeplink!, params.isDeferred);
}
};
Singular.start(config);
}
void handlePassthroughData(String passthroughString) {
try {
final data = jsonDecode(passthroughString);
// Apply promo code if present
if (data.containsKey('promo_code')) {
applyPromoCode(data['promo_code']);
}
// Set user segment
if (data.containsKey('segment')) {
setUserSegment(data['segment']);
}
// Track campaign
if (data.containsKey('campaign_id')) {
trackCampaign(data['campaign_id']);
}
} catch (error) {
print('Error parsing passthrough: $error');
}
}
void handleDeepLinkNavigation(String url, bool isDeferred) {
// Parse URL to extract route and parameters
final urlObj = parseDeepLink(url);
print('Navigating to: ${urlObj['route']}');
print('Parameters: ${urlObj['params']}');
print('Deferred install: $isDeferred');
// Route based on deep link structure
switch (urlObj['route']) {
case 'product':
navigateToProduct(urlObj['params']['id']);
break;
case 'promo':
navigateToPromo(urlObj['params']['code']);
break;
case 'category':
navigateToCategory(urlObj['params']['name']);
break;
default:
navigateToHome();
}
}
Map<String, dynamic> parseDeepLink(String url) {
// Parse myapp://product/123?variant=blue
final parts = url.split('?');
final path = parts[0];
final queryString = parts.length > 1 ? parts[1] : null;
// Extract path components
final pathParts = path.replaceFirst(RegExp(r'^[^:]+://'), '').split('/');
final route = pathParts[0];
// Parse parameters
final params = <String, String>{};
// Add path parameters
if (pathParts.length > 1) {
params['id'] = pathParts[1];
}
// Add query parameters
if (queryString != null) {
queryString.split('&').forEach((pair) {
final keyValue = pair.split('=');
if (keyValue.length == 2) {
params[keyValue[0]] = Uri.decodeComponent(keyValue[1]);
}
});
}
return {
'route': route,
'params': params
};
}
// Navigation functions
void navigateToProduct(String? productId) {
if (productId != null) {
print('Navigating to product: $productId');
// Use your navigation framework (Navigator, GoRouter, etc.)
navigatorKey.currentState?.pushNamed('/product/$productId');
}
}
void navigateToPromo(String? promoCode) {
if (promoCode != null) {
print('Navigating to promo: $promoCode');
navigatorKey.currentState?.pushNamed('/promo/$promoCode');
}
}
void navigateToCategory(String? categoryName) {
if (categoryName != null) {
print('Navigating to category: $categoryName');
navigatorKey.currentState?.pushNamed('/category/$categoryName');
}
}
void navigateToHome() {
print('Navigating to home');
navigatorKey.currentState?.pushNamed('/');
}
// Utility functions
void applyPromoCode(String code) {
print('Applying promo code: $code');
// Your promo code logic
}
void setUserSegment(String segment) {
print('Setting user segment: $segment');
// Your user segmentation logic
}
void trackCampaign(String campaignId) {
print('Tracking campaign: $campaignId');
// Your campaign tracking logic
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
navigatorKey: navigatorKey,
initialRoute: '/',
routes: {
'/': (context) => HomeScreen(),
'/product': (context) => ProductScreen(),
'/promo': (context) => PromoScreen(),
'/category': (context) => CategoryScreen(),
},
);
}
}
// Placeholder screen classes
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Home')),
body: Center(child: Text('Home Screen')),
);
}
}
class ProductScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Product')),
body: Center(child: Text('Product Screen')),
);
}
}
class PromoScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Promo')),
body: Center(child: Text('Promo Screen')),
);
}
}
class CategoryScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Category')),
body: Center(child: Text('Category Screen')),
);
}
}
Best Practices:
- Parse URLs Safely: Always validate and sanitize deep link URLs before navigation to prevent security vulnerabilities
- Handle Navigation State: Use GlobalKey for NavigatorState to enable navigation before MaterialApp fully initializes
- Test Both Scenarios: Test both immediate deep links (app installed) and deferred deep links (fresh install) during development
- Log for Debugging: Enable comprehensive logging during development to trace deep link resolution and navigation flow
- Error Handling: Implement try-catch blocks for JSON parsing and navigation operations to gracefully handle malformed data