{"id":394,"date":"2026-03-08T17:00:00","date_gmt":"2026-03-08T22:00:00","guid":{"rendered":"https:\/\/tolinku.com\/blog\/?p=394"},"modified":"2026-03-07T04:33:23","modified_gmt":"2026-03-07T09:33:23","slug":"cross-platform-deep-linking-guide","status":"publish","type":"post","link":"https:\/\/tolinku.com\/blog\/cross-platform-deep-linking-guide\/","title":{"rendered":"Cross-Platform Deep Linking Guide for 2026"},"content":{"rendered":"\n<p>Most mobile teams build with cross-platform frameworks. React Native and Flutter dominate, but Kotlin Multiplatform, .NET MAUI, and Capacitor are all growing. These frameworks let you write shared UI and business logic once, then ship to iOS and Android from a single codebase. Deep linking, however, is where the &quot;write once&quot; promise breaks down.<\/p>\n\n\n\n<p>Deep links require platform-specific configuration on both iOS and Android. You need Apple&#39;s <code>apple-app-site-association<\/code> file, Google&#39;s <code>assetlinks.json<\/code>, entitlements in Xcode, intent filters in the Android manifest, and framework-specific bridge code to receive those links in your Dart or JavaScript layer. Every cross-platform framework handles this differently, and none of them abstract it away completely.<\/p>\n\n\n\n<p>This guide covers cross-platform deep linking implementation across React Native, Flutter, Kotlin Multiplatform, .NET MAUI, and Capacitor. You&#39;ll find working code, native configuration requirements, navigation patterns, and testing strategies for each.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Cross-Platform Deep Linking Challenge<\/h2>\n\n\n\n<p>When a user taps a link on their phone, the operating system decides what happens. If the link matches a registered <a href=\"https:\/\/tolinku.com\/blog\/universal-links-everything-you-need-to-know\/\">Universal Link<\/a> (iOS) or <a href=\"https:\/\/tolinku.com\/blog\/android-app-links-complete-guide\/\">App Link<\/a> (Android), the OS opens the app directly. This decision happens at the OS level, before your framework code runs.<\/p>\n\n\n\n<p>That creates a layered problem:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Native configuration<\/strong>: Each platform needs its own manifest entries, entitlements, and server-hosted verification files.<\/li>\n<li><strong>Framework bridging<\/strong>: The native deep link event must cross the bridge into your JavaScript, Dart, or Kotlin code.<\/li>\n<li><strong>Navigation routing<\/strong>: Once the link arrives in your framework layer, you need to parse it and navigate to the correct screen.<\/li>\n<li><strong>Deferred deep links<\/strong>: If the app wasn&#39;t installed when the user tapped the link, you need a way to recover that context after installation.<\/li>\n<\/ol>\n\n\n\n<p>No cross-platform framework handles all four layers for you. Frameworks typically address layer 2 (bridging) and layer 3 (navigation), but layers 1 and 4 remain your responsibility.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Deep Linking in React Native<\/h2>\n\n\n\n<p>For a dedicated deep dive into React Native specifically, see our <a href=\"https:\/\/tolinku.com\/blog\/react-native-deep-linking-setup\/\">React Native deep linking setup guide<\/a>. React Native provides the <a href=\"https:\/\/reactnative.dev\/docs\/linking\" rel=\"nofollow noopener\" target=\"_blank\"><code>Linking<\/code> API<\/a> for receiving deep links. Combined with <a href=\"https:\/\/reactnavigation.org\/docs\/deep-linking\/\" rel=\"nofollow noopener\" target=\"_blank\">React Navigation&#39;s deep linking support<\/a>, you can wire up URL-to-screen mapping with minimal native code.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Setting Up the Linking API<\/h3>\n\n\n\n<p>React Navigation accepts a <code>linking<\/code> configuration that maps URL paths to screens:<\/p>\n\n\n\n<pre><code class=\"language-tsx\">\/\/ App.tsx\nimport { NavigationContainer } from &#39;@react-navigation\/native&#39;;\n\nconst linking = {\n  prefixes: [&#39;https:\/\/myapp.com&#39;, &#39;myapp:\/\/&#39;],\n  config: {\n    screens: {\n      Home: &#39;&#39;,\n      Product: &#39;product\/:id&#39;,\n      Profile: &#39;user\/:username&#39;,\n      Settings: {\n        path: &#39;settings&#39;,\n        screens: {\n          Notifications: &#39;notifications&#39;,\n          Privacy: &#39;privacy&#39;,\n        },\n      },\n    },\n  },\n};\n\nexport default function App() {\n  return (\n    &lt;NavigationContainer linking={linking} fallback={&lt;LoadingScreen \/&gt;}&gt;\n      &lt;RootNavigator \/&gt;\n    &lt;\/NavigationContainer&gt;\n  );\n}\n<\/code><\/pre>\n\n\n\n<p>When the app opens via <code>https:\/\/myapp.com\/product\/42<\/code>, React Navigation parses the URL, matches it to the <code>Product<\/code> screen, and passes <code>{ id: &#39;42&#39; }<\/code> as route params.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Handling Deep Links Manually<\/h3>\n\n\n\n<p>For cases where you need more control (authentication checks, analytics, redirects), you can intercept links before navigation:<\/p>\n\n\n\n<pre><code class=\"language-tsx\">import { Linking } from &#39;react-native&#39;;\nimport { useEffect } from &#39;react&#39;;\n\nfunction useDeepLinkHandler() {\n  useEffect(() =&gt; {\n    \/\/ Handle link that opened the app (cold start)\n    Linking.getInitialURL().then((url) =&gt; {\n      if (url) handleDeepLink(url);\n    });\n\n    \/\/ Handle link while app is in foreground (warm start)\n    const subscription = Linking.addEventListener(&#39;url&#39;, ({ url }) =&gt; {\n      handleDeepLink(url);\n    });\n\n    return () =&gt; subscription.remove();\n  }, []);\n}\n\nfunction handleDeepLink(url: string) {\n  const parsed = new URL(url);\n  const path = parsed.pathname;\n\n  \/\/ Check auth, log analytics, then navigate\n  if (path.startsWith(&#39;\/product\/&#39;)) {\n    const productId = path.split(&#39;\/&#39;)[2];\n    navigate(&#39;Product&#39;, { id: productId });\n  }\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Native Configuration for React Native<\/h3>\n\n\n\n<p>Even though your routing logic lives in JavaScript, you still need native setup.<\/p>\n\n\n\n<p><strong>iOS (Xcode):<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Enable the &quot;Associated Domains&quot; capability and add <code>applinks:myapp.com<\/code>.<\/li>\n<li>In <code>AppDelegate.mm<\/code>, ensure the <code>application:continueUserActivity:restorationHandler:<\/code> method passes Universal Links to React Native. Most React Native templates include this by default.<\/li>\n<\/ul>\n\n\n\n<p><strong>Android (<code>AndroidManifest.xml<\/code>):<\/strong><\/p>\n\n\n\n<pre><code class=\"language-xml\">&lt;activity\n  android:name=&quot;.MainActivity&quot;\n  android:launchMode=&quot;singleTask&quot;&gt;\n  &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\n      android:scheme=&quot;https&quot;\n      android:host=&quot;myapp.com&quot;\n      android:pathPrefix=&quot;\/product&quot; \/&gt;\n  &lt;\/intent-filter&gt;\n&lt;\/activity&gt;\n<\/code><\/pre>\n\n\n\n<p>The <code>android:autoVerify=&quot;true&quot;<\/code> attribute tells Android to check your <code>assetlinks.json<\/code> file at <code>https:\/\/myapp.com\/.well-known\/assetlinks.json<\/code> to verify ownership.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Deep Linking in Flutter<\/h2>\n\n\n\n<p>Flutter&#39;s deep linking story has matured significantly. For a complete Flutter-specific walkthrough, see our <a href=\"https:\/\/tolinku.com\/blog\/flutter-deep-linking-setup\/\">Flutter deep linking setup guide<\/a>. The recommended approach uses the <a href=\"https:\/\/pub.dev\/packages\/go_router\" rel=\"nofollow noopener\" target=\"_blank\"><code>go_router<\/code><\/a> package, which integrates directly with Flutter&#39;s <a href=\"https:\/\/docs.flutter.dev\/ui\/navigation\/deep-linking\" rel=\"nofollow noopener\" target=\"_blank\">Router API<\/a>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Configuring go_router<\/h3>\n\n\n\n<pre><code class=\"language-dart\">\/\/ router.dart\nimport &#39;package:go_router\/go_router.dart&#39;;\n\nfinal router = GoRouter(\n  routes: [\n    GoRoute(\n      path: &#39;\/&#39;,\n      builder: (context, state) =&gt; const HomeScreen(),\n    ),\n    GoRoute(\n      path: &#39;\/product\/:id&#39;,\n      builder: (context, state) {\n        final productId = state.pathParameters[&#39;id&#39;]!;\n        return ProductScreen(id: productId);\n      },\n    ),\n    GoRoute(\n      path: &#39;\/user\/:username&#39;,\n      builder: (context, state) {\n        final username = state.pathParameters[&#39;username&#39;]!;\n        return ProfileScreen(username: username);\n      },\n    ),\n  ],\n  errorBuilder: (context, state) =&gt; const NotFoundScreen(),\n);\n<\/code><\/pre>\n\n\n\n<pre><code class=\"language-dart\">\/\/ main.dart\nimport &#39;package:flutter\/material.dart&#39;;\n\nvoid main() {\n  runApp(\n    MaterialApp.router(\n      routerConfig: router,\n    ),\n  );\n}\n<\/code><\/pre>\n\n\n\n<p>When a deep link opens the app, Flutter&#39;s engine passes the URL to <code>go_router<\/code>, which matches the path and renders the correct screen. No manual <code>MethodChannel<\/code> code is needed for basic deep linking.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Handling Deep Links with Guards<\/h3>\n\n\n\n<p>For authenticated routes or deferred link processing, use <code>go_router<\/code>&#39;s redirect feature:<\/p>\n\n\n\n<pre><code class=\"language-dart\">final router = GoRouter(\n  redirect: (context, state) {\n    final isLoggedIn = authService.isLoggedIn;\n    final isGoingToLogin = state.matchedLocation == &#39;\/login&#39;;\n\n    if (!isLoggedIn &amp;&amp; !isGoingToLogin) {\n      \/\/ Store the deep link destination for after login\n      authService.pendingDeepLink = state.uri.toString();\n      return &#39;\/login&#39;;\n    }\n\n    if (isLoggedIn &amp;&amp; isGoingToLogin) {\n      return authService.pendingDeepLink ?? &#39;\/&#39;;\n    }\n\n    return null; \/\/ no redirect\n  },\n  routes: [ \/* ... *\/ ],\n);\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Native Configuration for Flutter<\/h3>\n\n\n\n<p><strong>iOS<\/strong>: Add <code>FlutterDeepLinkingEnabled<\/code> as a boolean <code>YES<\/code> in your <code>Info.plist<\/code>, and configure Associated Domains in <code>Runner.entitlements<\/code>:<\/p>\n\n\n\n<pre><code class=\"language-xml\">&lt;!-- ios\/Runner\/Info.plist --&gt;\n&lt;key&gt;FlutterDeepLinkingEnabled&lt;\/key&gt;\n&lt;true\/&gt;\n<\/code><\/pre>\n\n\n\n<p><strong>Android<\/strong>: Add intent filters in <code>AndroidManifest.xml<\/code>, just like with React Native. Flutter reads the incoming intent automatically when <code>FlutterDeepLinkingEnabled<\/code> is configured (or when using <code>go_router<\/code>, which handles it internally).<\/p>\n\n\n\n<p>Flutter&#39;s documentation on <a href=\"https:\/\/docs.flutter.dev\/cookbook\/navigation\/set-up-app-links\" rel=\"nofollow noopener\" target=\"_blank\">deep linking setup<\/a> walks through each step in detail.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Deep Linking in Other Frameworks<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Kotlin Multiplatform (KMP)<\/h3>\n\n\n\n<p>Kotlin Multiplatform shares business logic across iOS and Android while keeping native UIs. Deep link handling typically stays in the platform-specific layer, but you can share the URL parsing logic:<\/p>\n\n\n\n<pre><code class=\"language-kotlin\">\/\/ shared\/src\/commonMain\/kotlin\/DeepLinkParser.kt\ndata class DeepLinkRoute(\n    val screen: String,\n    val params: Map&lt;String, String&gt;\n)\n\nfun parseDeepLink(url: String): DeepLinkRoute? {\n    val path = url.substringAfter(&quot;myapp.com&quot;)\n        .substringBefore(&quot;?&quot;)\n        .trim(&#39;\/&#39;)\n\n    val segments = path.split(&quot;\/&quot;)\n    return when {\n        segments.size == 2 &amp;&amp; segments[0] == &quot;product&quot; -&gt;\n            DeepLinkRoute(&quot;product&quot;, mapOf(&quot;id&quot; to segments[1]))\n        segments.size == 2 &amp;&amp; segments[0] == &quot;user&quot; -&gt;\n            DeepLinkRoute(&quot;profile&quot;, mapOf(&quot;username&quot; to segments[1]))\n        else -&gt; null\n    }\n}\n<\/code><\/pre>\n\n\n\n<p>Each platform calls <code>parseDeepLink()<\/code> from its native deep link handler and routes accordingly. On Android, you call it from <code>onCreate()<\/code> or <code>onNewIntent()<\/code>. On iOS, you call it from <code>application(_:continue:restorationHandler:)<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">.NET MAUI<\/h3>\n\n\n\n<p>.NET MAUI uses the <code>AppAction<\/code> API and custom URI schemes. For App Links and Universal Links, you configure platform-specific handlers in the <code>Platforms\/<\/code> directories and route through the shared <code>App.xaml.cs<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Capacitor (Ionic)<\/h3>\n\n\n\n<p>Capacitor wraps a web app in a native shell. The <a href=\"https:\/\/capacitorjs.com\/docs\/apis\/app\" rel=\"nofollow noopener\" target=\"_blank\"><code>@capacitor\/app<\/code><\/a> plugin provides <code>appUrlOpen<\/code> events:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">import { App } from &#39;@capacitor\/app&#39;;\n\nApp.addListener(&#39;appUrlOpen&#39;, (event) =&gt; {\n  const url = new URL(event.url);\n  const path = url.pathname;\n\n  if (path.startsWith(&#39;\/product\/&#39;)) {\n    router.push(path);\n  }\n});\n<\/code><\/pre>\n\n\n\n<p>Capacitor still requires the same native configuration (Associated Domains on iOS, intent filters on Android). The web layer just receives the URL after the native shell intercepts it.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Native Configuration You Cannot Skip<\/h2>\n\n\n\n<p>Regardless of your framework, two server-side files are mandatory for verified deep links.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Apple App Site Association (AASA)<\/h3>\n\n\n\n<p>Hosted at <code>https:\/\/yourdomain.com\/.well-known\/apple-app-site-association<\/code>:<\/p>\n\n\n\n<pre><code class=\"language-json\">{\n  &quot;applinks&quot;: {\n    &quot;details&quot;: [\n      {\n        &quot;appIDs&quot;: [&quot;TEAMID.com.yourcompany.yourapp&quot;],\n        &quot;components&quot;: [\n          { &quot;\/&quot;: &quot;\/product\/*&quot;, &quot;comment&quot;: &quot;Product pages&quot; },\n          { &quot;\/&quot;: &quot;\/user\/*&quot;, &quot;comment&quot;: &quot;User profiles&quot; }\n        ]\n      }\n    ]\n  }\n}\n<\/code><\/pre>\n\n\n\n<p>Apple&#39;s CDN fetches this file when your app is installed (and periodically after). If it&#39;s misconfigured, Universal Links won&#39;t work at all. The file must be served with <code>Content-Type: application\/json<\/code> and must not require authentication or redirect. See Apple&#39;s <a href=\"https:\/\/developer.apple.com\/documentation\/xcode\/supporting-universal-links-in-your-app\" rel=\"nofollow noopener\" target=\"_blank\">Universal Links documentation<\/a> for the full specification.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Android Asset Links<\/h3>\n\n\n\n<p>Hosted at <code>https:\/\/yourdomain.com\/.well-known\/assetlinks.json<\/code>:<\/p>\n\n\n\n<pre><code class=\"language-json\">[{\n  &quot;relation&quot;: [&quot;delegate_permission\/common.handle_all_urls&quot;],\n  &quot;target&quot;: {\n    &quot;namespace&quot;: &quot;android_app_namespace&quot;,\n    &quot;package_name&quot;: &quot;com.yourcompany.yourapp&quot;,\n    &quot;sha256_cert_fingerprints&quot;: [\n      &quot;AA:BB:CC:...&quot;\n    ]\n  }\n}]\n<\/code><\/pre>\n\n\n\n<p>Android verifies this file during app installation. The SHA-256 fingerprint must match your app&#39;s signing certificate. Debug builds use a different certificate than release builds, so you&#39;ll need both fingerprints during development. Google&#39;s <a href=\"https:\/\/developer.android.com\/training\/app-links\/verify-android-applinks\" rel=\"nofollow noopener\" target=\"_blank\">App Links documentation<\/a> covers the verification process.<\/p>\n\n\n\n<p>These files live on your web server, not in your app. Cross-platform frameworks don&#39;t generate or manage them. This is one reason teams use a deep linking service: it handles the verification files, fallback logic, and routing configuration in one place, so your server setup stays minimal.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Navigation Architecture for Deep Links<\/h2>\n\n\n\n<p>The biggest mistake in cross-platform deep linking is scattering link-handling logic across multiple screens or components. Instead, centralize your routing.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The Centralized Router Pattern<\/h3>\n\n\n\n<p>Define a single function or module that maps URLs to navigation actions:<\/p>\n\n\n\n<pre><code class=\"language-tsx\">\/\/ deepLinkRouter.ts (React Native)\ntype RouteHandler = (params: Record&lt;string, string&gt;) =&gt; void;\n\nconst routes: Array&lt;{ pattern: RegExp; handler: RouteHandler }&gt; = [\n  {\n    pattern: \/^\\\/product\\\/([a-zA-Z0-9]+)$\/,\n    handler: (params) =&gt; navigate(&#39;Product&#39;, { id: params.id }),\n  },\n  {\n    pattern: \/^\\\/user\\\/([a-zA-Z0-9_]+)$\/,\n    handler: (params) =&gt; navigate(&#39;Profile&#39;, { username: params.username }),\n  },\n  {\n    pattern: \/^\\\/invite\\\/([a-zA-Z0-9]+)$\/,\n    handler: (params) =&gt; navigate(&#39;AcceptInvite&#39;, { code: params.code }),\n  },\n];\n\nexport function routeDeepLink(url: string): boolean {\n  const path = new URL(url).pathname;\n\n  for (const route of routes) {\n    const match = path.match(route.pattern);\n    if (match) {\n      const params = extractNamedParams(match);\n      route.handler(params);\n      return true;\n    }\n  }\n  return false; \/\/ no match\n}\n<\/code><\/pre>\n\n\n\n<p>This pattern works identically in Dart, Kotlin, or TypeScript. The router is the single source of truth for URL-to-screen mapping. When you add a new deep link, you add one entry here instead of touching multiple files.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Typed Route Parameters<\/h3>\n\n\n\n<p>Type safety prevents an entire class of deep link bugs. In TypeScript projects, define your route params as types:<\/p>\n\n\n\n<pre><code class=\"language-tsx\">type DeepLinkRoutes = {\n  Product: { id: string };\n  Profile: { username: string };\n  Settings: undefined;\n  Order: { orderId: string; tab?: &#39;details&#39; | &#39;tracking&#39; };\n};\n<\/code><\/pre>\n\n\n\n<p>In Dart, use classes or records:<\/p>\n\n\n\n<pre><code class=\"language-dart\">sealed class DeepLinkRoute {\n  const DeepLinkRoute();\n}\n\nclass ProductRoute extends DeepLinkRoute {\n  final String id;\n  const ProductRoute(this.id);\n}\n\nclass ProfileRoute extends DeepLinkRoute {\n  final String username;\n  const ProfileRoute(this.username);\n}\n<\/code><\/pre>\n\n\n\n<p>Typed routes make it impossible to navigate to a product screen without an ID, catching mistakes at compile time rather than in production.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Handling Deferred Deep Links in Cross-Platform Apps<\/h2>\n\n\n\n<p>Deferred deep linking is the hardest part of cross-platform deep linking. When a user taps a link but doesn&#39;t have your app installed, the link context needs to survive the entire install flow: redirect to the app store, download, install, and first launch.<\/p>\n\n\n\n<p>Cross-platform frameworks provide no built-in support for deferred deep links. The app store install process erases URL context entirely. Recovering that context requires a server-side matching service that correlates the pre-install click with the post-install app open using device characteristics (IP address, user agent, device model, timestamp).<\/p>\n\n\n\n<p>Implementing deferred deep linking from scratch involves:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Recording click metadata on your server when the user taps the link.<\/li>\n<li>Redirecting to the appropriate app store.<\/li>\n<li>On first app launch, sending device metadata to your server.<\/li>\n<li>Matching the first-launch request against recent clicks.<\/li>\n<li>Returning the original deep link destination to the app.<\/li>\n<\/ol>\n\n\n\n<p>The matching logic needs to handle edge cases: shared Wi-Fi networks (same IP, different devices), VPN users, and time delays between click and install. Probabilistic matching using multiple signals works well in practice, but it requires ongoing maintenance.<\/p>\n\n\n\n<p>This is where a dedicated deep linking service saves real engineering time. <a href=\"https:\/\/tolinku.com\/features\/deep-linking\">Tolinku&#39;s deep linking features<\/a> handle deferred deep links out of the box, including the server-side matching, fallback pages, and SDK integration for retrieving the deferred link on first launch.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Testing Cross-Platform Deep Links<\/h2>\n\n\n\n<p>Testing deep links is notoriously tedious because you can&#39;t test them in a simulator&#39;s browser the same way they work on a real device. Here&#39;s a framework-by-framework testing strategy.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">React Native<\/h3>\n\n\n\n<p>Use <code>npx uri-scheme<\/code> to trigger deep links from the command line:<\/p>\n\n\n\n<pre><code class=\"language-bash\"># iOS Simulator\nxcrun simctl openurl booted &quot;https:\/\/myapp.com\/product\/42&quot;\n\n# Android Emulator\nadb shell am start -a android.intent.action.VIEW \\\n  -d &quot;https:\/\/myapp.com\/product\/42&quot; \\\n  com.yourcompany.yourapp\n<\/code><\/pre>\n\n\n\n<p>React Navigation also provides a <a href=\"https:\/\/reactnavigation.org\/docs\/navigation-container\/#linkingcontext\" rel=\"nofollow noopener\" target=\"_blank\"><code>LinkingContext<\/code><\/a> for unit testing link resolution without needing a device.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Flutter<\/h3>\n\n\n\n<p>Flutter provides the <code>--route<\/code> flag for testing initial routes:<\/p>\n\n\n\n<pre><code class=\"language-bash\">flutter run --route &quot;\/product\/42&quot;\n<\/code><\/pre>\n\n\n\n<p>For integration tests, use <code>WidgetTester<\/code> to verify navigation:<\/p>\n\n\n\n<pre><code class=\"language-dart\">testWidgets(&#39;deep link navigates to product&#39;, (tester) async {\n  await tester.pumpWidget(\n    MaterialApp.router(routerConfig: router),\n  );\n\n  router.go(&#39;\/product\/42&#39;);\n  await tester.pumpAndSettle();\n\n  expect(find.byType(ProductScreen), findsOneWidget);\n});\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">CI\/CD Integration<\/h3>\n\n\n\n<p>Automate deep link testing in your CI pipeline:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Run Android emulator tests with <code>adb shell am start<\/code> commands.<\/li>\n<li>Run iOS simulator tests with <code>xcrun simctl openurl<\/code>.<\/li>\n<li>Validate your AASA file with Apple&#39;s <a href=\"https:\/\/search.developer.apple.com\/appsearch-validation-tool\/\" rel=\"nofollow noopener\" target=\"_blank\">search validation tool<\/a>.<\/li>\n<li>Validate <code>assetlinks.json<\/code> with Google&#39;s <a href=\"https:\/\/developers.google.com\/digital-asset-links\/tools\/generator\" rel=\"nofollow noopener\" target=\"_blank\">Statement List Tester<\/a>.<\/li>\n<li>Include URL parsing unit tests for every supported deep link pattern.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Using Tolinku SDKs for Cross-Platform Deep Linking<\/h2>\n\n\n\n<p>If you&#39;d rather not maintain verification files, fallback logic, deferred deep linking infrastructure, and per-platform configuration yourself, <a href=\"https:\/\/tolinku.com\/docs\/developer\/sdks\/\">Tolinku&#39;s SDKs<\/a> handle the heavy lifting.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">React Native SDK<\/h3>\n\n\n\n<p>The <a href=\"https:\/\/tolinku.com\/docs\/developer\/sdks\/react-native\/\">Tolinku React Native SDK<\/a> wraps the native iOS and Android SDKs and exposes a unified JavaScript API:<\/p>\n\n\n\n<pre><code class=\"language-tsx\">import { Tolinku } from &#39;@tolinku\/react-native-sdk&#39;;\n\n\/\/ Initialize once at app start\nTolinku.init({ apiKey: &#39;tolk_pub_your_key&#39; });\n\n\/\/ Handle deferred deep links on first launch\nTolinku.getInitialLink().then((link) =&gt; {\n  if (link) {\n    routeDeepLink(link.url);\n  }\n});\n\n\/\/ Listen for deep links while app is running\nTolinku.onLink((link) =&gt; {\n  routeDeepLink(link.url);\n});\n<\/code><\/pre>\n\n\n\n<p>The SDK handles the native Universal Links and App Links integration, deferred deep link resolution, and link metadata retrieval. Your routing logic stays in JavaScript.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Flutter SDK<\/h3>\n\n\n\n<p>The <a href=\"https:\/\/tolinku.com\/docs\/developer\/sdks\/flutter\/\">Tolinku Flutter SDK<\/a> follows similar patterns using Dart streams:<\/p>\n\n\n\n<pre><code class=\"language-dart\">import &#39;package:tolinku\/tolinku.dart&#39;;\n\nvoid main() async {\n  WidgetsFlutterBinding.ensureInitialized();\n  await Tolinku.init(apiKey: &#39;tolk_pub_your_key&#39;);\n\n  \/\/ Check for deferred deep link\n  final initialLink = await Tolinku.getInitialLink();\n  if (initialLink != null) {\n    \/\/ Route to the intended screen\n  }\n\n  runApp(const MyApp());\n}\n<\/code><\/pre>\n\n\n\n<p>Both SDKs also work alongside the native <a href=\"https:\/\/tolinku.com\/docs\/developer\/sdks\/ios\/\">iOS SDK<\/a> and <a href=\"https:\/\/tolinku.com\/docs\/developer\/sdks\/android\/\">Android SDK<\/a>, so if you ever move to fully native development, the same link infrastructure carries over. The <a href=\"https:\/\/tolinku.com\/docs\/developer\/sdks\/web\/\">Web SDK<\/a> covers the web side, handling smart banners and link routing for your mobile web traffic.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Best Practices for Cross-Platform Deep Links<\/h2>\n\n\n\n<p><strong>1. Keep URL patterns consistent across platforms.<\/strong> If <code>\/product\/:id<\/code> works on iOS, it should work on Android, and it should work on the web too. Use the same path structure everywhere.<\/p>\n\n\n\n<p><strong>2. Handle missing or invalid parameters gracefully.<\/strong> Deep links come from external sources: emails, push notifications, SMS, social media, QR codes. Users (and bots) will send malformed URLs. Validate parameters and show a helpful fallback screen instead of crashing.<\/p>\n\n\n\n<pre><code class=\"language-tsx\">function ProductScreen({ route }) {\n  const { id } = route.params;\n\n  if (!id || !isValidProductId(id)) {\n    return &lt;NotFoundScreen message=&quot;Product not found&quot; \/&gt;;\n  }\n\n  return &lt;ProductDetail id={id} \/&gt;;\n}\n<\/code><\/pre>\n\n\n\n<p><strong>3. Test on real devices, not just simulators.<\/strong> Universal Links behave differently on physical devices than in the iOS Simulator. The Simulator doesn&#39;t fully replicate the AASA verification flow. Budget time for real-device testing before release.<\/p>\n\n\n\n<p><strong>4. Version your deep link routes.<\/strong> When you rename a screen or change URL structure, keep the old paths working. Broken deep links in emails or saved bookmarks create a terrible user experience. Map old paths to new destinations rather than removing them.<\/p>\n\n\n\n<p><strong>5. Log deep link events.<\/strong> Track which deep links users tap, whether they resolve successfully, and where failures happen. This data is essential for debugging and for understanding which entry points drive the most engagement.<\/p>\n\n\n\n<p><strong>6. Don&#39;t rely on custom URL schemes for primary deep linking.<\/strong> Custom schemes (<code>myapp:\/\/<\/code>) have significant limitations: they don&#39;t work in many contexts (embedded browsers, some email clients), they can conflict with other apps, and they show error dialogs when the app isn&#39;t installed. Use HTTPS Universal Links and App Links as your primary deep linking mechanism.<\/p>\n\n\n\n<p><strong>7. Handle the cold start vs. warm start distinction.<\/strong> When the app is already running and receives a deep link, the behavior differs from a cold start. React Native&#39;s <code>Linking.getInitialURL()<\/code> only fires on cold start; the <code>url<\/code> event listener only fires on warm start. Flutter&#39;s <code>go_router<\/code> handles both, but if you&#39;re using manual platform channels, test both scenarios explicitly.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p>Cross-platform deep linking requires work on multiple layers: server-side verification files, native platform configuration, framework bridge code, and in-app navigation routing. No single framework abstracts all of this away, and that&#39;s unlikely to change because the native platform requirements are fundamental to how mobile operating systems handle links.<\/p>\n\n\n\n<p>The practical approach is to standardize your URL patterns early, centralize your routing logic, type your route parameters, and test on real devices. For deferred deep links, you&#39;ll almost certainly want a service that handles the server-side matching rather than building it yourself.<\/p>\n\n\n\n<p>Whether you&#39;re building with React Native, Flutter, Kotlin Multiplatform, or any other cross-platform framework, the deep linking fundamentals are the same. The differences are in the syntax, not the architecture. Get the architecture right, and switching frameworks or adding new platforms becomes a matter of writing new bridge code, not redesigning your link infrastructure.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Implement deep linking across React Native, Flutter, and other cross-platform frameworks. One guide for all hybrid and native approaches.<\/p>\n","protected":false},"author":2,"featured_media":393,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Cross-Platform Deep Linking Guide for 2026","rank_math_description":"Implement deep linking across React Native, Flutter, and other cross-platform frameworks. One guide for all hybrid and native approaches.","rank_math_focus_keyword":"cross-platform deep linking","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-cross-platform-deep-linking-guide.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-cross-platform-deep-linking-guide.png","footnotes":""},"categories":[15],"tags":[25,20,57,24,34,56,30,31],"class_list":["post-394","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-engineering","tag-android","tag-deep-linking","tag-flutter","tag-ios","tag-kotlin","tag-react-native","tag-sdks","tag-swift"],"_links":{"self":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/394","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=394"}],"version-history":[{"count":4,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/394\/revisions"}],"predecessor-version":[{"id":2765,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/394\/revisions\/2765"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media\/393"}],"wp:attachment":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media?parent=394"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/categories?post=394"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/tags?post=394"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}