Most deep link routes work identically on iOS and Android: the same URL maps to the same screen. But some routes need platform-specific behavior. iOS may support features Android does not (Handoff, Spotlight indexing). Android may use Intent-based navigation that has no iOS equivalent. This article covers patterns for handling these differences while keeping your codebase maintainable.
For shared routing patterns, see cross-platform link handling patterns. For the full overview, see cross-platform deep linking guide for 2026.
Where Platform Differences Appear
Deep link routing has platform-specific behavior at three levels:
1. URL Reception
iOS and Android receive deep links through different APIs:
| Aspect | iOS | Android |
|---|---|---|
| Mechanism | Universal Links (AASA) | App Links (assetlinks.json) |
| Entry point | application(_:continue:restorationHandler:) |
onCreate() / onNewIntent() with Intent |
| URL format | Same HTTPS URL | Same HTTPS URL |
| Verification | AASA file | assetlinks.json |
2. Navigation
Each platform's navigation system works differently:
- iOS uses
UINavigationControllerpush/pop,UITabBarController, or SwiftUINavigationStack. - Android uses Activities, Fragments, and the Navigation component.
- Cross-platform frameworks (React Native, Flutter, Capacitor) abstract these but sometimes expose differences.
3. Feature Availability
Some features only exist on one platform:
- iOS only: Handoff, Spotlight indexing, Universal Clipboard
- Android only: Instant Apps, Custom Tabs fallback, multi-window deep links
- Behavioral differences: Back button handling, task stack management, split-screen behavior
Pattern: Platform-Aware Route Configuration
Define routes with optional platform-specific overrides:
interface Route {
path: string;
screen: string;
platforms: ('ios' | 'android' | 'web')[];
iosConfig?: {
handoffActivity?: string;
spotlightTitle?: string;
};
androidConfig?: {
taskAffinity?: string;
launchMode?: 'singleTop' | 'singleTask';
};
}
const routes: Route[] = [
{
path: '/products/:productId',
screen: 'ProductDetail',
platforms: ['ios', 'android', 'web']
},
{
path: '/ar-preview/:productId',
screen: 'ARPreview',
platforms: ['ios'], // ARKit only
iosConfig: {
spotlightTitle: 'AR Preview'
}
},
{
path: '/instant/:productId',
screen: 'InstantPreview',
platforms: ['android'], // Android Instant Apps
androidConfig: {
launchMode: 'singleTask'
}
},
{
path: '/share/:shareId',
screen: 'SharedContent',
platforms: ['ios', 'android', 'web'],
iosConfig: {
handoffActivity: 'com.yourapp.viewing'
}
}
];
Implementing Platform-Specific Routing
Shared Router with Platform Filter
function matchRoute(
url: string,
platform: 'ios' | 'android' | 'web'
): RouteMatch | null {
const parsed = new URL(url);
const path = parsed.pathname;
for (const route of routes) {
// Skip routes not available on this platform
if (!route.platforms.includes(platform)) continue;
const match = matchPath(path, route.path);
if (match) {
return {
screen: route.screen,
params: {
...match.params,
...Object.fromEntries(parsed.searchParams)
},
platformConfig: platform === 'ios'
? route.iosConfig
: route.androidConfig
};
}
}
return null;
}
function matchPath(
actual: string,
pattern: string
): { params: Record<string, string> } | null {
const paramNames: string[] = [];
const regex = new RegExp(
'^' + pattern.replace(/:([^/]+)/g, (_, name) => {
paramNames.push(name);
return '([^/]+)';
}) + '$'
);
const match = actual.match(regex);
if (!match) return null;
const params: Record<string, string> = {};
paramNames.forEach((name, i) => {
params[name] = match[i + 1];
});
return { params };
}
iOS Implementation
func handleDeepLink(_ url: URL) {
guard let route = DeepLinkRouter.match(
url: url.absoluteString,
platform: "ios"
) else {
navigateToHome()
return
}
// Handle iOS-specific config
if let handoffActivity = route.platformConfig?.handoffActivity {
setupHandoff(activityType: handoffActivity, url: url)
}
navigateTo(screen: route.screen, params: route.params)
}
private func setupHandoff(activityType: String, url: URL) {
let activity = NSUserActivity(activityType: activityType)
activity.webpageURL = url
activity.isEligibleForHandoff = true
UIApplication.shared.userActivity = activity
}
Android Implementation
fun handleDeepLink(intent: Intent) {
val uri = intent.data ?: return
val route = DeepLinkRouter.match(
url = uri.toString(),
platform = "android"
) ?: run {
navigateToHome()
return
}
// Handle Android-specific config
route.platformConfig?.launchMode?.let { mode ->
when (mode) {
"singleTask" -> {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
"singleTop" -> {
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
}
}
navigateTo(screen = route.screen, params = route.params)
}
Handling Fallbacks Between Platforms
When a deep link targets a platform-specific feature, define fallback behavior for the other platform:
interface Route {
path: string;
screen: string;
platforms: ('ios' | 'android' | 'web')[];
fallback?: {
screen: string;
message?: string;
};
}
const routes: Route[] = [
{
path: '/ar-preview/:productId',
screen: 'ARPreview',
platforms: ['ios'],
fallback: {
screen: 'ProductDetail',
message: 'AR preview is only available on iOS'
}
}
];
function resolveRoute(url: string, platform: string): RouteMatch {
const route = findMatchingRoute(url);
if (!route) {
return { screen: 'Home', params: {} };
}
if (!route.platforms.includes(platform)) {
// Platform not supported, use fallback
if (route.fallback) {
return {
screen: route.fallback.screen,
params: extractParams(url, route.path),
message: route.fallback.message
};
}
return { screen: 'Home', params: {} };
}
return {
screen: route.screen,
params: extractParams(url, route.path)
};
}
AASA and assetlinks.json Alignment
Platform-specific routes affect your verification files. You only need to list paths in the verification file for the platform that supports them.
AASA (iOS)
Include only iOS and shared routes:
{
"applinks": {
"details": [{
"appIDs": ["TEAMID.com.yourcompany.yourapp"],
"components": [
{ "/": "/products/*" },
{ "/": "/offers/*" },
{ "/": "/ar-preview/*" },
{ "/": "/share/*" }
]
}]
}
}
AndroidManifest.xml
Include only Android and shared routes:
<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="yourdomain.com"
android:pathPrefix="/products" />
<data android:scheme="https"
android:host="yourdomain.com"
android:pathPrefix="/offers" />
<data android:scheme="https"
android:host="yourdomain.com"
android:pathPrefix="/instant" />
<data android:scheme="https"
android:host="yourdomain.com"
android:pathPrefix="/share" />
</intent-filter>
Back Navigation Differences
After a deep link opens, back button behavior differs significantly:
iOS
- The system provides a "Back to [source app]" link in the status bar.
- Your app's navigation stack starts fresh (unless the app was already running).
- Users expect swiping back to go to the parent screen, not the source app.
func navigateFromDeepLink(to screen: String, params: [String: String]) {
// Build a navigation stack so the user can navigate back
let homeVC = HomeViewController()
let detailVC = makeViewController(screen: screen, params: params)
navigationController?.setViewControllers([homeVC, detailVC], animated: false)
}
Android
- The back button should follow the app's task stack.
- Use
TaskStackBuilderto create a proper back stack:
fun navigateFromDeepLink(screen: String, params: Map<String, String>) {
val detailIntent = makeIntent(screen, params)
TaskStackBuilder.create(this)
.addNextIntentWithParentStack(detailIntent)
.startActivities()
}
Cross-Platform Framework Considerations
React Native
React Native's Linking API abstracts the platform entry point, but you may still need platform checks for specific behaviors:
import { Platform, Linking } from 'react-native';
function handleDeepLink(url: string) {
const route = matchRoute(url, Platform.OS);
if (!route) {
navigation.navigate('Home');
return;
}
// Platform-specific handling
if (Platform.OS === 'ios' && route.iosConfig?.spotlightTitle) {
// Index in Spotlight
indexInSpotlight(route);
}
navigation.navigate(route.screen, route.params);
}
Flutter
Flutter's route handling is platform-agnostic, but platform channels can be used for platform-specific features:
void handleDeepLink(Uri uri) {
final route = matchRoute(uri.toString());
if (route == null) {
Navigator.of(context).pushReplacementNamed('/');
return;
}
// Platform-specific features via method channel
if (Platform.isIOS && route.handoffActivity != null) {
_methodChannel.invokeMethod('setupHandoff', {
'activityType': route.handoffActivity,
'url': uri.toString(),
});
}
Navigator.of(context).pushNamed(
route.screen,
arguments: route.params,
);
}
Testing Platform-Specific Routes
Test each platform independently:
# iOS: test an iOS-only route
xcrun simctl openurl booted "https://yourdomain.com/ar-preview/product123"
# Android: test an Android-only route
adb shell am start -a android.intent.action.VIEW \
-d "https://yourdomain.com/instant/product123" \
-c android.intent.category.BROWSABLE
# Test shared route on both
xcrun simctl openurl booted "https://yourdomain.com/products/abc123"
adb shell am start -a android.intent.action.VIEW \
-d "https://yourdomain.com/products/abc123" \
-c android.intent.category.BROWSABLE
Test fallback behavior by sending platform-specific URLs to the wrong platform:
# Send iOS-only route to Android (should fallback)
adb shell am start -a android.intent.action.VIEW \
-d "https://yourdomain.com/ar-preview/product123" \
-c android.intent.category.BROWSABLE
Tolinku for Multi-Platform Routing
Tolinku hosts both AASA and assetlinks.json verification files. You configure your iOS and Android app details in the Appspace settings, and Tolinku generates the correct verification files for each platform. Routes can include dynamic parameters that work consistently across platforms.
For routing fundamentals, see deep link routing: how to route users to the right screen. For shared routing logic, see cross-platform link handling patterns.
Get deep linking tips in your inbox
One email per week. No spam.