{"id":1885,"date":"2026-07-29T17:00:00","date_gmt":"2026-07-29T22:00:00","guid":{"rendered":"https:\/\/tolinku.com\/blog\/?p=1885"},"modified":"2026-03-07T03:50:17","modified_gmt":"2026-03-07T08:50:17","slug":"platform-specific-link-routing","status":"publish","type":"post","link":"https:\/\/tolinku.com\/blog\/platform-specific-link-routing\/","title":{"rendered":"Platform-Specific Link Routing in Cross-Platform Apps"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For shared routing patterns, see <a href=\"https:\/\/tolinku.com\/blog\/cross-platform-link-handling\/\">cross-platform link handling patterns<\/a>. For the full overview, see <a href=\"https:\/\/tolinku.com\/blog\/cross-platform-deep-linking-guide\/\">cross-platform deep linking guide for 2026<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Where Platform Differences Appear<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Deep link routing has platform-specific behavior at three levels:<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">1. URL Reception<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">iOS and Android receive deep links through different APIs:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead>\n<tr>\n<th>Aspect<\/th>\n<th>iOS<\/th>\n<th>Android<\/th>\n<\/tr>\n<\/thead>\n<tbody><tr>\n<td>Mechanism<\/td>\n<td>Universal Links (AASA)<\/td>\n<td>App Links (assetlinks.json)<\/td>\n<\/tr>\n<tr>\n<td>Entry point<\/td>\n<td><code>application(_:continue:restorationHandler:)<\/code><\/td>\n<td><code>onCreate()<\/code> \/ <code>onNewIntent()<\/code> with Intent<\/td>\n<\/tr>\n<tr>\n<td>URL format<\/td>\n<td>Same HTTPS URL<\/td>\n<td>Same HTTPS URL<\/td>\n<\/tr>\n<tr>\n<td>Verification<\/td>\n<td><a href=\"https:\/\/developer.apple.com\/documentation\/bundleresources\/applinks\" rel=\"nofollow noopener\" target=\"_blank\">AASA file<\/a><\/td>\n<td><a href=\"https:\/\/developer.android.com\/training\/app-links\/verify-android-applinks\" rel=\"nofollow noopener\" target=\"_blank\">assetlinks.json<\/a><\/td>\n<\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">2. Navigation<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Each platform&#39;s navigation system works differently:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>iOS<\/strong> uses <code>UINavigationController<\/code> push\/pop, <code>UITabBarController<\/code>, or SwiftUI <code>NavigationStack<\/code>.<\/li>\n<li><strong>Android<\/strong> uses Activities, Fragments, and the <a href=\"https:\/\/developer.android.com\/guide\/navigation\" rel=\"nofollow noopener\" target=\"_blank\">Navigation component<\/a>.<\/li>\n<li><strong>Cross-platform frameworks<\/strong> (React Native, Flutter, Capacitor) abstract these but sometimes expose differences.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">3. Feature Availability<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Some features only exist on one platform:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>iOS only<\/strong>: Handoff, Spotlight indexing, Universal Clipboard<\/li>\n<li><strong>Android only<\/strong>: Instant Apps, Custom Tabs fallback, multi-window deep links<\/li>\n<li><strong>Behavioral differences<\/strong>: Back button handling, task stack management, split-screen behavior<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Pattern: Platform-Aware Route Configuration<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Define routes with optional platform-specific overrides:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">interface Route {\n  path: string;\n  screen: string;\n  platforms: (&#39;ios&#39; | &#39;android&#39; | &#39;web&#39;)[];\n  iosConfig?: {\n    handoffActivity?: string;\n    spotlightTitle?: string;\n  };\n  androidConfig?: {\n    taskAffinity?: string;\n    launchMode?: &#39;singleTop&#39; | &#39;singleTask&#39;;\n  };\n}\n\nconst routes: Route[] = [\n  {\n    path: &#39;\/products\/:productId&#39;,\n    screen: &#39;ProductDetail&#39;,\n    platforms: [&#39;ios&#39;, &#39;android&#39;, &#39;web&#39;]\n  },\n  {\n    path: &#39;\/ar-preview\/:productId&#39;,\n    screen: &#39;ARPreview&#39;,\n    platforms: [&#39;ios&#39;], \/\/ ARKit only\n    iosConfig: {\n      spotlightTitle: &#39;AR Preview&#39;\n    }\n  },\n  {\n    path: &#39;\/instant\/:productId&#39;,\n    screen: &#39;InstantPreview&#39;,\n    platforms: [&#39;android&#39;], \/\/ Android Instant Apps\n    androidConfig: {\n      launchMode: &#39;singleTask&#39;\n    }\n  },\n  {\n    path: &#39;\/share\/:shareId&#39;,\n    screen: &#39;SharedContent&#39;,\n    platforms: [&#39;ios&#39;, &#39;android&#39;, &#39;web&#39;],\n    iosConfig: {\n      handoffActivity: &#39;com.yourapp.viewing&#39;\n    }\n  }\n];\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Implementing Platform-Specific Routing<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Shared Router with Platform Filter<\/h3>\n\n\n\n<pre><code class=\"language-typescript\">function matchRoute(\n  url: string,\n  platform: &#39;ios&#39; | &#39;android&#39; | &#39;web&#39;\n): RouteMatch | null {\n  const parsed = new URL(url);\n  const path = parsed.pathname;\n\n  for (const route of routes) {\n    \/\/ Skip routes not available on this platform\n    if (!route.platforms.includes(platform)) continue;\n\n    const match = matchPath(path, route.path);\n    if (match) {\n      return {\n        screen: route.screen,\n        params: {\n          ...match.params,\n          ...Object.fromEntries(parsed.searchParams)\n        },\n        platformConfig: platform === &#39;ios&#39;\n          ? route.iosConfig\n          : route.androidConfig\n      };\n    }\n  }\n\n  return null;\n}\n\nfunction matchPath(\n  actual: string,\n  pattern: string\n): { params: Record&lt;string, string&gt; } | null {\n  const paramNames: string[] = [];\n  const regex = new RegExp(\n    &#39;^&#39; + pattern.replace(\/:([^\/]+)\/g, (_, name) =&gt; {\n      paramNames.push(name);\n      return &#39;([^\/]+)&#39;;\n    }) + &#39;$&#39;\n  );\n\n  const match = actual.match(regex);\n  if (!match) return null;\n\n  const params: Record&lt;string, string&gt; = {};\n  paramNames.forEach((name, i) =&gt; {\n    params[name] = match[i + 1];\n  });\n  return { params };\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">iOS Implementation<\/h3>\n\n\n\n<pre><code class=\"language-swift\">func handleDeepLink(_ url: URL) {\n    guard let route = DeepLinkRouter.match(\n        url: url.absoluteString,\n        platform: &quot;ios&quot;\n    ) else {\n        navigateToHome()\n        return\n    }\n\n    \/\/ Handle iOS-specific config\n    if let handoffActivity = route.platformConfig?.handoffActivity {\n        setupHandoff(activityType: handoffActivity, url: url)\n    }\n\n    navigateTo(screen: route.screen, params: route.params)\n}\n\nprivate func setupHandoff(activityType: String, url: URL) {\n    let activity = NSUserActivity(activityType: activityType)\n    activity.webpageURL = url\n    activity.isEligibleForHandoff = true\n    UIApplication.shared.userActivity = activity\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Android Implementation<\/h3>\n\n\n\n<pre><code class=\"language-kotlin\">fun handleDeepLink(intent: Intent) {\n    val uri = intent.data ?: return\n    val route = DeepLinkRouter.match(\n        url = uri.toString(),\n        platform = &quot;android&quot;\n    ) ?: run {\n        navigateToHome()\n        return\n    }\n\n    \/\/ Handle Android-specific config\n    route.platformConfig?.launchMode?.let { mode -&gt;\n        when (mode) {\n            &quot;singleTask&quot; -&gt; {\n                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or\n                    Intent.FLAG_ACTIVITY_CLEAR_TOP)\n            }\n            &quot;singleTop&quot; -&gt; {\n                intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)\n            }\n        }\n    }\n\n    navigateTo(screen = route.screen, params = route.params)\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Handling Fallbacks Between Platforms<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">When a deep link targets a platform-specific feature, define fallback behavior for the other platform:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">interface Route {\n  path: string;\n  screen: string;\n  platforms: (&#39;ios&#39; | &#39;android&#39; | &#39;web&#39;)[];\n  fallback?: {\n    screen: string;\n    message?: string;\n  };\n}\n\nconst routes: Route[] = [\n  {\n    path: &#39;\/ar-preview\/:productId&#39;,\n    screen: &#39;ARPreview&#39;,\n    platforms: [&#39;ios&#39;],\n    fallback: {\n      screen: &#39;ProductDetail&#39;,\n      message: &#39;AR preview is only available on iOS&#39;\n    }\n  }\n];\n\nfunction resolveRoute(url: string, platform: string): RouteMatch {\n  const route = findMatchingRoute(url);\n\n  if (!route) {\n    return { screen: &#39;Home&#39;, params: {} };\n  }\n\n  if (!route.platforms.includes(platform)) {\n    \/\/ Platform not supported, use fallback\n    if (route.fallback) {\n      return {\n        screen: route.fallback.screen,\n        params: extractParams(url, route.path),\n        message: route.fallback.message\n      };\n    }\n    return { screen: &#39;Home&#39;, params: {} };\n  }\n\n  return {\n    screen: route.screen,\n    params: extractParams(url, route.path)\n  };\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">AASA and assetlinks.json Alignment<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Platform-specific routes affect your verification files. You only need to list paths in the verification file for the platform that supports them.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">AASA (iOS)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Include only iOS and shared routes:<\/p>\n\n\n\n<pre><code class=\"language-json\">{\n  &quot;applinks&quot;: {\n    &quot;details&quot;: [{\n      &quot;appIDs&quot;: [&quot;TEAMID.com.yourcompany.yourapp&quot;],\n      &quot;components&quot;: [\n        { &quot;\/&quot;: &quot;\/products\/*&quot; },\n        { &quot;\/&quot;: &quot;\/offers\/*&quot; },\n        { &quot;\/&quot;: &quot;\/ar-preview\/*&quot; },\n        { &quot;\/&quot;: &quot;\/share\/*&quot; }\n      ]\n    }]\n  }\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">AndroidManifest.xml<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Include only Android and shared routes:<\/p>\n\n\n\n<pre><code class=\"language-xml\">&lt;intent-filter android:autoVerify=&quot;true&quot;&gt;\n    &lt;action android:name=&quot;android.intent.action.VIEW&quot; \/&gt;\n    &lt;category android:name=&quot;android.intent.category.DEFAULT&quot; \/&gt;\n    &lt;category android:name=&quot;android.intent.category.BROWSABLE&quot; \/&gt;\n    &lt;data android:scheme=&quot;https&quot;\n          android:host=&quot;yourdomain.com&quot;\n          android:pathPrefix=&quot;\/products&quot; \/&gt;\n    &lt;data android:scheme=&quot;https&quot;\n          android:host=&quot;yourdomain.com&quot;\n          android:pathPrefix=&quot;\/offers&quot; \/&gt;\n    &lt;data android:scheme=&quot;https&quot;\n          android:host=&quot;yourdomain.com&quot;\n          android:pathPrefix=&quot;\/instant&quot; \/&gt;\n    &lt;data android:scheme=&quot;https&quot;\n          android:host=&quot;yourdomain.com&quot;\n          android:pathPrefix=&quot;\/share&quot; \/&gt;\n&lt;\/intent-filter&gt;\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Back Navigation Differences<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">After a deep link opens, back button behavior differs significantly:<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">iOS<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The system provides a &quot;Back to [source app]&quot; link in the status bar.<\/li>\n<li>Your app&#39;s navigation stack starts fresh (unless the app was already running).<\/li>\n<li>Users expect swiping back to go to the parent screen, not the source app.<\/li>\n<\/ul>\n\n\n\n<pre><code class=\"language-swift\">func navigateFromDeepLink(to screen: String, params: [String: String]) {\n    \/\/ Build a navigation stack so the user can navigate back\n    let homeVC = HomeViewController()\n    let detailVC = makeViewController(screen: screen, params: params)\n\n    navigationController?.setViewControllers([homeVC, detailVC], animated: false)\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Android<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The back button should follow the <a href=\"https:\/\/developer.android.com\/guide\/components\/activities\/tasks-and-back-stack\" rel=\"nofollow noopener\" target=\"_blank\">app&#39;s task stack<\/a>.<\/li>\n<li>Use <code>TaskStackBuilder<\/code> to create a proper back stack:<\/li>\n<\/ul>\n\n\n\n<pre><code class=\"language-kotlin\">fun navigateFromDeepLink(screen: String, params: Map&lt;String, String&gt;) {\n    val detailIntent = makeIntent(screen, params)\n\n    TaskStackBuilder.create(this)\n        .addNextIntentWithParentStack(detailIntent)\n        .startActivities()\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Cross-Platform Framework Considerations<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">React Native<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">React Native&#39;s <a href=\"https:\/\/reactnative.dev\/docs\/linking\" rel=\"nofollow noopener\" target=\"_blank\">Linking API<\/a> abstracts the platform entry point, but you may still need platform checks for specific behaviors:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">import { Platform, Linking } from &#39;react-native&#39;;\n\nfunction handleDeepLink(url: string) {\n  const route = matchRoute(url, Platform.OS);\n\n  if (!route) {\n    navigation.navigate(&#39;Home&#39;);\n    return;\n  }\n\n  \/\/ Platform-specific handling\n  if (Platform.OS === &#39;ios&#39; &amp;&amp; route.iosConfig?.spotlightTitle) {\n    \/\/ Index in Spotlight\n    indexInSpotlight(route);\n  }\n\n  navigation.navigate(route.screen, route.params);\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Flutter<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Flutter&#39;s route handling is platform-agnostic, but platform channels can be used for platform-specific features:<\/p>\n\n\n\n<pre><code class=\"language-dart\">void handleDeepLink(Uri uri) {\n  final route = matchRoute(uri.toString());\n\n  if (route == null) {\n    Navigator.of(context).pushReplacementNamed(&#39;\/&#39;);\n    return;\n  }\n\n  \/\/ Platform-specific features via method channel\n  if (Platform.isIOS &amp;&amp; route.handoffActivity != null) {\n    _methodChannel.invokeMethod(&#39;setupHandoff&#39;, {\n      &#39;activityType&#39;: route.handoffActivity,\n      &#39;url&#39;: uri.toString(),\n    });\n  }\n\n  Navigator.of(context).pushNamed(\n    route.screen,\n    arguments: route.params,\n  );\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Testing Platform-Specific Routes<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Test each platform independently:<\/p>\n\n\n\n<pre><code class=\"language-bash\"># iOS: test an iOS-only route\nxcrun simctl openurl booted &quot;https:\/\/yourdomain.com\/ar-preview\/product123&quot;\n\n# Android: test an Android-only route\nadb shell am start -a android.intent.action.VIEW \\\n  -d &quot;https:\/\/yourdomain.com\/instant\/product123&quot; \\\n  -c android.intent.category.BROWSABLE\n\n# Test shared route on both\nxcrun simctl openurl booted &quot;https:\/\/yourdomain.com\/products\/abc123&quot;\nadb shell am start -a android.intent.action.VIEW \\\n  -d &quot;https:\/\/yourdomain.com\/products\/abc123&quot; \\\n  -c android.intent.category.BROWSABLE\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Test fallback behavior by sending platform-specific URLs to the wrong platform:<\/p>\n\n\n\n<pre><code class=\"language-bash\"># Send iOS-only route to Android (should fallback)\nadb shell am start -a android.intent.action.VIEW \\\n  -d &quot;https:\/\/yourdomain.com\/ar-preview\/product123&quot; \\\n  -c android.intent.category.BROWSABLE\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Tolinku for Multi-Platform Routing<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/tolinku.com\/features\/deep-linking\">Tolinku<\/a> hosts both AASA and assetlinks.json verification files. You configure your iOS and Android app details in the <a href=\"https:\/\/tolinku.com\/docs\/user-guide\/appspace-settings\/\">Appspace settings<\/a>, and Tolinku generates the correct verification files for each platform. <a href=\"https:\/\/tolinku.com\/docs\/user-guide\/routes\/\">Routes<\/a> can include <a href=\"https:\/\/tolinku.com\/docs\/user-guide\/routes\/dynamic-routes\/\">dynamic parameters<\/a> that work consistently across platforms.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For routing fundamentals, see <a href=\"https:\/\/tolinku.com\/blog\/deep-link-routing-guide\/\">deep link routing: how to route users to the right screen<\/a>. For shared routing logic, see <a href=\"https:\/\/tolinku.com\/blog\/cross-platform-link-handling\/\">cross-platform link handling patterns<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Handle platform differences in deep link routing. Manage iOS-only, Android-only, and shared routes in cross-platform codebases.<\/p>\n","protected":false},"author":2,"featured_media":1884,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Platform-Specific Link Routing in Cross-Platform Apps","rank_math_description":"Handle platform differences in deep link routing. Manage iOS-only, Android-only, and shared routes in cross-platform codebases.","rank_math_focus_keyword":"platform-specific routing","rank_math_canonical_url":"","rank_math_facebook_title":"","rank_math_facebook_description":"","rank_math_facebook_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-platform-specific-link-routing.png","rank_math_facebook_image_id":"","rank_math_twitter_title":"","rank_math_twitter_description":"","rank_math_twitter_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-platform-specific-link-routing.png","footnotes":""},"categories":[15],"tags":[25,23,156,20,24,34,69,183,31,22],"class_list":["post-1885","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-engineering","tag-android","tag-app-links","tag-cross-platform","tag-deep-linking","tag-ios","tag-kotlin","tag-mobile-development","tag-routing","tag-swift","tag-universal-links"],"_links":{"self":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1885","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/comments?post=1885"}],"version-history":[{"count":1,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1885\/revisions"}],"predecessor-version":[{"id":1886,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1885\/revisions\/1886"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media\/1884"}],"wp:attachment":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media?parent=1885"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/categories?post=1885"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/tags?post=1885"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}