{"id":1906,"date":"2026-08-01T09:00:00","date_gmt":"2026-08-01T14:00:00","guid":{"rendered":"https:\/\/tolinku.com\/blog\/?p=1906"},"modified":"2026-03-07T03:50:19","modified_gmt":"2026-03-07T08:50:19","slug":"app-links-flutter-android","status":"publish","type":"post","link":"https:\/\/tolinku.com\/blog\/app-links-flutter-android\/","title":{"rendered":"Handling App Links in Flutter Android"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Flutter Android apps receive App Links through the same Android mechanisms as native apps: Intent filters in AndroidManifest.xml, Digital Asset Links verification via assetlinks.json, and Activity lifecycle methods. Flutter adds its own layer: the <code>FlutterActivity<\/code> receives the Intent, and your Dart code handles routing. This article covers the Android-specific configuration.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For the full Flutter App Links guide (iOS and Android), see <a href=\"https:\/\/tolinku.com\/blog\/flutter-app-links\/\">App Links in Flutter: Android configuration guide<\/a>. For Android Manifest details, see <a href=\"https:\/\/tolinku.com\/blog\/android-manifest-deep-link-configuration\/\">Android Manifest configuration for deep links<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Prerequisites<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Flutter 3.19+ (latest stable recommended)<\/li>\n<li>A domain you control with HTTPS<\/li>\n<li>assetlinks.json hosted at <code>https:\/\/yourdomain.com\/.well-known\/assetlinks.json<\/code><\/li>\n<li>Your app&#39;s signing key SHA-256 fingerprint<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Step 1: AndroidManifest Configuration<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Edit <code>android\/app\/src\/main\/AndroidManifest.xml<\/code>. Add Intent filters to the <code>&lt;activity&gt;<\/code> element that contains <code>android:name=&quot;.MainActivity&quot;<\/code>:<\/p>\n\n\n\n<pre><code class=\"language-xml\">&lt;activity\n    android:name=&quot;.MainActivity&quot;\n    android:exported=&quot;true&quot;\n    android:launchMode=&quot;singleTop&quot;\n    android:theme=&quot;@style\/LaunchTheme&quot;&gt;\n\n    &lt;!-- Default Flutter intent filters --&gt;\n    &lt;meta-data\n        android:name=&quot;io.flutter.embedding.android.NormalTheme&quot;\n        android:resource=&quot;@style\/NormalTheme&quot; \/&gt;\n\n    &lt;intent-filter&gt;\n        &lt;action android:name=&quot;android.intent.action.MAIN&quot;\/&gt;\n        &lt;category android:name=&quot;android.intent.category.LAUNCHER&quot;\/&gt;\n    &lt;\/intent-filter&gt;\n\n    &lt;!-- App Links --&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 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;\/referral&quot; \/&gt;\n    &lt;\/intent-filter&gt;\n&lt;\/activity&gt;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Key attributes:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong><code>android:autoVerify=&quot;true&quot;<\/code><\/strong>: Tells Android to verify ownership via Digital Asset Links. Without this, Android shows a disambiguation dialog instead of opening the app directly.<\/li>\n<li><strong><code>android:launchMode=&quot;singleTop&quot;<\/code><\/strong>: Prevents creating a new Activity when the app is already running. The existing Activity receives the new Intent via <code>onNewIntent<\/code>.<\/li>\n<li><strong><code>android:exported=&quot;true&quot;<\/code><\/strong>: Required for the Activity to receive Intents from external apps.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Step 2: Digital Asset Links (assetlinks.json)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Host at <code>https:\/\/yourdomain.com\/.well-known\/assetlinks.json<\/code>:<\/p>\n\n\n\n<pre><code class=\"language-json\">[\n  {\n    &quot;relation&quot;: [&quot;delegate_permission\/common.handle_all_urls&quot;],\n    &quot;target&quot;: {\n      &quot;namespace&quot;: &quot;android_app&quot;,\n      &quot;package_name&quot;: &quot;com.yourcompany.yourapp&quot;,\n      &quot;sha256_cert_fingerprints&quot;: [\n        &quot;YOUR_DEBUG_KEY_FINGERPRINT&quot;,\n        &quot;YOUR_RELEASE_KEY_FINGERPRINT&quot;\n      ]\n    }\n  }\n]\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Getting Your Fingerprints<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Debug key<\/strong> (local development):<\/p>\n\n\n\n<pre><code class=\"language-bash\">keytool -list -v -keystore ~\/.android\/debug.keystore \\\n  -alias androiddebugkey -storepass android 2&gt;\/dev\/null | grep SHA256\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Release key<\/strong> (if using Play App Signing):<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Find it in Google Play Console under Setup &gt; App signing &gt; App signing key certificate &gt; SHA-256 fingerprint.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Release key<\/strong> (if signing locally):<\/p>\n\n\n\n<pre><code class=\"language-bash\">keytool -list -v -keystore \/path\/to\/your-release-key.jks \\\n  -alias your-key-alias\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Include both debug and release fingerprints during development. For production, you can remove the debug fingerprint.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Verify the File<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Use Google&#39;s verification tool:<\/p>\n\n\n\n<pre><code class=\"language-bash\"># Check if your assetlinks.json is valid\ncurl -s &quot;https:\/\/digitalassetlinks.googleapis.com\/v1\/statements:list?source.web.site=https:\/\/yourdomain.com&amp;relation=delegate_permission\/common.handle_all_urls&quot; | python3 -m json.tool\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Or use the <a href=\"https:\/\/developers.google.com\/digital-asset-links\/tools\/generator\" rel=\"nofollow noopener\" target=\"_blank\">Statement List Generator<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 3: Dart-Side Handling<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Using <code>app_links<\/code> Package<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The <a href=\"https:\/\/pub.dev\/packages\/app_links\" rel=\"nofollow noopener\" target=\"_blank\"><code>app_links<\/code><\/a> package is the recommended way to handle App Links in Flutter:<\/p>\n\n\n\n<pre><code class=\"language-yaml\"># pubspec.yaml\ndependencies:\n  app_links: ^6.3.0\n<\/code><\/pre>\n\n\n\n<pre><code class=\"language-dart\">import &#39;package:app_links\/app_links.dart&#39;;\nimport &#39;package:flutter\/material.dart&#39;;\n\nclass MyApp extends StatefulWidget {\n  @override\n  State&lt;MyApp&gt; createState() =&gt; _MyAppState();\n}\n\nclass _MyAppState extends State&lt;MyApp&gt; {\n  late final AppLinks _appLinks;\n\n  @override\n  void initState() {\n    super.initState();\n    _appLinks = AppLinks();\n    _initDeepLinks();\n  }\n\n  Future&lt;void&gt; _initDeepLinks() async {\n    \/\/ Cold start: check if the app was launched from a deep link\n    final initialUri = await _appLinks.getInitialLink();\n    if (initialUri != null) {\n      _handleDeepLink(initialUri);\n    }\n\n    \/\/ Warm start: listen for new deep links\n    _appLinks.uriLinkStream.listen((uri) {\n      _handleDeepLink(uri);\n    });\n  }\n\n  void _handleDeepLink(Uri uri) {\n    final path = uri.path;\n\n    final productMatch = RegExp(r&#39;^\/products\/([^\/]+)$&#39;).firstMatch(path);\n    if (productMatch != null) {\n      final productId = productMatch.group(1)!;\n      Navigator.of(context).pushNamed(\n        &#39;\/products\/detail&#39;,\n        arguments: {&#39;productId&#39;: productId},\n      );\n      return;\n    }\n\n    final offerMatch = RegExp(r&#39;^\/offers\/([^\/]+)$&#39;).firstMatch(path);\n    if (offerMatch != null) {\n      final offerId = offerMatch.group(1)!;\n      Navigator.of(context).pushNamed(\n        &#39;\/offers\/detail&#39;,\n        arguments: {&#39;offerId&#39;: offerId},\n      );\n      return;\n    }\n\n    final referralMatch = RegExp(r&#39;^\/referral\/([^\/]+)$&#39;).firstMatch(path);\n    if (referralMatch != null) {\n      final referrerId = referralMatch.group(1)!;\n      _handleReferral(referrerId);\n      return;\n    }\n  }\n\n  void _handleReferral(String referrerId) {\n    \/\/ Store the referral for attribution\n    \/\/ Navigate to welcome screen\n    Navigator.of(context).pushNamed(\n      &#39;\/referral&#39;,\n      arguments: {&#39;referrerId&#39;: referrerId},\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\/* ... *\/);\n  }\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Using GoRouter<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/pub.dev\/packages\/go_router\" rel=\"nofollow noopener\" target=\"_blank\">GoRouter<\/a> supports deep linking natively:<\/p>\n\n\n\n<pre><code class=\"language-dart\">import &#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 HomePage(),\n    ),\n    GoRoute(\n      path: &#39;\/products\/:productId&#39;,\n      builder: (context, state) {\n        final productId = state.pathParameters[&#39;productId&#39;]!;\n        final ref = state.uri.queryParameters[&#39;ref&#39;];\n        return ProductDetailPage(\n          productId: productId,\n          referralSource: ref,\n        );\n      },\n    ),\n    GoRoute(\n      path: &#39;\/offers\/:offerId&#39;,\n      builder: (context, state) {\n        final offerId = state.pathParameters[&#39;offerId&#39;]!;\n        return OfferDetailPage(offerId: offerId);\n      },\n    ),\n    GoRoute(\n      path: &#39;\/referral\/:referrerId&#39;,\n      builder: (context, state) {\n        final referrerId = state.pathParameters[&#39;referrerId&#39;]!;\n        return ReferralPage(referrerId: referrerId);\n      },\n    ),\n  ],\n);\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp.router(\n      routerConfig: router,\n    );\n  }\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">GoRouter automatically handles both cold start and warm start deep links when used with <code>MaterialApp.router<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 4: Verify App Links Work<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">On Emulator<\/h3>\n\n\n\n<pre><code class=\"language-bash\"># Test a product deep link\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\n# Test an offer deep link\nadb shell am start -a android.intent.action.VIEW \\\n  -d &quot;https:\/\/yourdomain.com\/offers\/deal456&quot; \\\n  -c android.intent.category.BROWSABLE\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If the app opens directly, App Links verification succeeded. If a disambiguation dialog appears (&quot;Open with&#8230;&quot;), verification failed.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Check Verification Status<\/h3>\n\n\n\n<pre><code class=\"language-bash\"># List all verified domains for your app\nadb shell pm get-app-links com.yourcompany.yourapp\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Output shows verification status:<\/p>\n\n\n\n<pre><code>com.yourcompany.yourapp:\n    ID: abc12345\n    Signatures: [...]\n    Domains:\n      yourdomain.com: verified\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If the domain shows <code>none<\/code> or <code>legacy_failure<\/code>, the assetlinks.json is not valid or not accessible.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Force Re-Verification<\/h3>\n\n\n\n<pre><code class=\"language-bash\"># Clear existing verification state\nadb shell pm set-app-links --package com.yourcompany.yourapp 0 all\n\n# Trigger re-verification\nadb shell pm verify-app-links --re-verify com.yourcompany.yourapp\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Common Issues<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead>\n<tr>\n<th>Issue<\/th>\n<th>Cause<\/th>\n<th>Fix<\/th>\n<\/tr>\n<\/thead>\n<tbody><tr>\n<td>Disambiguation dialog instead of direct open<\/td>\n<td><code>autoVerify<\/code> failed<\/td>\n<td>Check assetlinks.json, verify fingerprint matches<\/td>\n<\/tr>\n<tr>\n<td>App opens but blank screen<\/td>\n<td>Route not matched<\/td>\n<td>Add logging to deep link handler, check path matching<\/td>\n<\/tr>\n<tr>\n<td>Cold start deep link missed<\/td>\n<td>Not checking <code>getInitialLink()<\/code><\/td>\n<td>Call <code>_appLinks.getInitialLink()<\/code> in <code>initState<\/code><\/td>\n<\/tr>\n<tr>\n<td>Works on debug but not release<\/td>\n<td>Wrong fingerprint in assetlinks.json<\/td>\n<td>Add release key fingerprint (or Play App Signing fingerprint)<\/td>\n<\/tr>\n<tr>\n<td>Deep link works once, then stops<\/td>\n<td>Activity recreated instead of reused<\/td>\n<td>Set <code>android:launchMode=&quot;singleTop&quot;<\/code><\/td>\n<\/tr>\n<tr>\n<td>Query parameters lost<\/td>\n<td>Parser strips query string<\/td>\n<td>Use <code>uri.queryParameters<\/code> to access them<\/td>\n<\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Debug Logging<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Add logging to trace the deep link flow:<\/p>\n\n\n\n<pre><code class=\"language-dart\">void _handleDeepLink(Uri uri) {\n  debugPrint(&#39;Deep link received: $uri&#39;);\n  debugPrint(&#39;Path: ${uri.path}&#39;);\n  debugPrint(&#39;Query: ${uri.queryParameters}&#39;);\n\n  \/\/ ... routing logic\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Multiple Path Prefixes<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If your app has many deep link paths, group them logically in the Intent filters:<\/p>\n\n\n\n<pre><code class=\"language-xml\">&lt;!-- Product-related links --&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 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;\/categories&quot; \/&gt;\n&lt;\/intent-filter&gt;\n\n&lt;!-- User-related links --&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 android:scheme=&quot;https&quot;\n          android:host=&quot;yourdomain.com&quot;\n          android:pathPrefix=&quot;\/referral&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;\/intent-filter&gt;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Note: you can use multiple <code>&lt;data&gt;<\/code> elements in a single <code>&lt;intent-filter&gt;<\/code>. Android combines the scheme, host, and pathPrefix from all <code>&lt;data&gt;<\/code> elements.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Tolinku for Flutter Android<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/tolinku.com\/features\/deep-linking\">Tolinku<\/a> hosts your assetlinks.json file automatically. Configure your Android package name and signing key fingerprints in the <a href=\"https:\/\/tolinku.com\/docs\/user-guide\/appspace-settings\/\">Appspace settings<\/a>, and the assetlinks.json is generated and served from your domain. The <a href=\"https:\/\/tolinku.com\/docs\/developer\/sdks\/flutter\/\">Flutter SDK<\/a> provides deferred deep linking and analytics.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For the full Flutter setup, see <a href=\"https:\/\/tolinku.com\/blog\/flutter-app-links\/\">App Links in Flutter: Android configuration guide<\/a>. For the cross-platform overview, see <a href=\"https:\/\/tolinku.com\/blog\/cross-platform-deep-linking-guide\/\">cross-platform deep linking guide for 2026<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Deep dive into App Links handling in Flutter Android. Configure AndroidManifest, verify Digital Asset Links, and route in Dart code.<\/p>\n","protected":false},"author":2,"featured_media":1905,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Handling App Links in Flutter Android","rank_math_description":"Deep dive into App Links handling in Flutter Android. Configure AndroidManifest, verify Digital Asset Links, and route in Dart code.","rank_math_focus_keyword":"app links Flutter Android","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-app-links-flutter-android.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-app-links-flutter-android.png","footnotes":""},"categories":[15],"tags":[25,580,23,92,156,179,20,35,57,69],"class_list":["post-1906","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-engineering","tag-android","tag-android-manifest","tag-app-links","tag-assetlinks","tag-cross-platform","tag-dart","tag-deep-linking","tag-digital-asset-links","tag-flutter","tag-mobile-development"],"_links":{"self":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1906","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=1906"}],"version-history":[{"count":1,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1906\/revisions"}],"predecessor-version":[{"id":1907,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1906\/revisions\/1907"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media\/1905"}],"wp:attachment":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media?parent=1906"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/categories?post=1906"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/tags?post=1906"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}