{"id":902,"date":"2026-04-25T09:00:00","date_gmt":"2026-04-25T14:00:00","guid":{"rendered":"https:\/\/tolinku.com\/blog\/?p=902"},"modified":"2026-03-07T03:48:34","modified_gmt":"2026-03-07T08:48:34","slug":"flutter-app-links","status":"publish","type":"post","link":"https:\/\/tolinku.com\/blog\/flutter-app-links\/","title":{"rendered":"App Links in Flutter: Android Configuration Guide"},"content":{"rendered":"\n<p>Android App Links let your Flutter app handle HTTPS links without showing a disambiguation dialog. When a user taps a link to your domain, the app opens directly. This guide covers the Android-specific configuration for App Links in Flutter apps.<\/p>\n\n\n\n<p>For the full Flutter deep linking setup (including iOS), see <a href=\"https:\/\/tolinku.com\/blog\/flutter-deep-linking-setup\/\">Flutter Deep Linking: Complete Setup Tutorial<\/a>. For go_router integration, see <a href=\"https:\/\/tolinku.com\/blog\/flutter-go-router-deep-links\/\">Flutter go_router Deep Links<\/a>. For the Android fundamentals, see <a href=\"https:\/\/tolinku.com\/blog\/android-app-links-complete-guide\/\">Android App Links: Complete Implementation Guide<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">How App Links Differ from Deep Links<\/h2>\n\n\n\n<p>Android has two types of link handling:<\/p>\n\n\n\n<p><strong>Deep Links<\/strong> (basic): Use <code>intent-filter<\/code> without <code>autoVerify<\/code>. When the user taps a matching link, Android shows a dialog asking which app should open it. Other apps can also claim the same URLs.<\/p>\n\n\n\n<p><strong>App Links<\/strong> (verified): Use <code>intent-filter<\/code> with <code>android:autoVerify=&quot;true&quot;<\/code>. Android verifies that you own the domain by checking <code>\/.well-known\/assetlinks.json<\/code>. Once verified, your app opens directly with no dialog.<\/p>\n\n\n\n<p>Always use App Links for production. Deep Links without verification are only appropriate for development and testing.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 1: Configure AndroidManifest.xml<\/h2>\n\n\n\n<p>Open <code>android\/app\/src\/main\/AndroidManifest.xml<\/code> and add intent filters to your main activity:<\/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;\n  android:configChanges=&quot;orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode&quot;\n  android:hardwareAccelerated=&quot;true&quot;\n  android:windowSoftInputMode=&quot;adjustResize&quot;&gt;\n\n  &lt;!-- Flutter deep linking metadata --&gt;\n  &lt;meta-data\n    android:name=&quot;flutter_deeplinking_enabled&quot;\n    android:value=&quot;true&quot; \/&gt;\n\n  &lt;!-- Default Flutter intent filter --&gt;\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 (verified) --&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;go.yourapp.com&quot; \/&gt;\n  &lt;\/intent-filter&gt;\n&lt;\/activity&gt;\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Key Attributes<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong><code>android:autoVerify=&quot;true&quot;<\/code><\/strong>: Triggers Android&#39;s domain verification process. Without this, links show a disambiguation dialog.<\/li>\n<li><strong><code>android:launchMode=&quot;singleTop&quot;<\/code><\/strong>: Reuses the existing activity when a link is tapped while the app is already running. Without this, a new activity is created, and the deep link may not reach your Dart code.<\/li>\n<li><strong><code>flutter_deeplinking_enabled<\/code><\/strong>: Tells Flutter&#39;s engine to forward incoming URLs to your Dart router automatically.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Multiple Domains<\/h3>\n\n\n\n<p>If your links come from multiple domains:<\/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; android:host=&quot;go.yourapp.com&quot; \/&gt;\n  &lt;data android:scheme=&quot;https&quot; android:host=&quot;links.yourapp.com&quot; \/&gt;\n&lt;\/intent-filter&gt;\n<\/code><\/pre>\n\n\n\n<p>Note: each domain you add requires its own <code>assetlinks.json<\/code> file.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Path Restrictions<\/h3>\n\n\n\n<p>To handle only specific paths (not all URLs on the domain):<\/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\n    android:scheme=&quot;https&quot;\n    android:host=&quot;go.yourapp.com&quot;\n    android:pathPrefix=&quot;\/product&quot; \/&gt;\n  &lt;data\n    android:scheme=&quot;https&quot;\n    android:host=&quot;go.yourapp.com&quot;\n    android:pathPrefix=&quot;\/invite&quot; \/&gt;\n&lt;\/intent-filter&gt;\n<\/code><\/pre>\n\n\n\n<p>Available path matching options:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>android:path=&quot;\/exact\/path&quot;<\/code> &#8211; Exact match<\/li>\n<li><code>android:pathPrefix=&quot;\/prefix&quot;<\/code> &#8211; Starts with<\/li>\n<li><code>android:pathPattern=&quot;\/product\/.*&quot;<\/code> &#8211; Regex pattern<\/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>Your domain must serve a verification file at:<\/p>\n\n\n\n<pre><code>https:\/\/go.yourapp.com\/.well-known\/assetlinks.json\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">File Content<\/h3>\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&quot;,\n    &quot;package_name&quot;: &quot;com.yourapp&quot;,\n    &quot;sha256_cert_fingerprints&quot;: [\n      &quot;AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99&quot;\n    ]\n  }\n}]\n<\/code><\/pre>\n\n\n\n<p>If using Tolinku, the assetlinks.json is generated automatically when you configure your Appspace with your package name and SHA-256 fingerprints.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Getting Your SHA-256 Fingerprint<\/h3>\n\n\n\n<p><strong>Debug keystore:<\/strong><\/p>\n\n\n\n<pre><code class=\"language-bash\">keytool -list -v -keystore ~\/.android\/debug.keystore -alias androiddebugkey -storepass android\n<\/code><\/pre>\n\n\n\n<p><strong>Release keystore:<\/strong><\/p>\n\n\n\n<pre><code class=\"language-bash\">keytool -list -v -keystore \/path\/to\/your\/release.keystore -alias your-alias\n<\/code><\/pre>\n\n\n\n<p><strong>Google Play App Signing:<\/strong>\nIf you use Google Play&#39;s app signing feature (recommended), Google re-signs your APK with their own key. You need both:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Your upload key fingerprint (from your keystore)<\/li>\n<li>Google&#39;s app signing key fingerprint (from Play Console: Setup &gt; App signing &gt; App signing key certificate)<\/li>\n<\/ol>\n\n\n\n<p>Include both in your assetlinks.json:<\/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&quot;,\n    &quot;package_name&quot;: &quot;com.yourapp&quot;,\n    &quot;sha256_cert_fingerprints&quot;: [\n      &quot;YOUR:UPLOAD:KEY:FINGERPRINT&quot;,\n      &quot;GOOGLE:SIGNING:KEY:FINGERPRINT&quot;\n    ]\n  }\n}]\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Verification<\/h3>\n\n\n\n<p>Check that the file is accessible and valid:<\/p>\n\n\n\n<pre><code class=\"language-bash\"># Accessibility\ncurl -I https:\/\/go.yourapp.com\/.well-known\/assetlinks.json\n\n# Content\ncurl https:\/\/go.yourapp.com\/.well-known\/assetlinks.json | python3 -m json.tool\n<\/code><\/pre>\n\n\n\n<p>Use <a href=\"https:\/\/developers.google.com\/digital-asset-links\/tools\/generator\" rel=\"nofollow noopener\" target=\"_blank\">Google&#39;s Digital Asset Links tool<\/a> to validate the file against your app.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 3: Handling Links in Dart<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">With go_router (Recommended)<\/h3>\n\n\n\n<pre><code class=\"language-dart\">final 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        return ProductScreen(id: state.pathParameters[&#39;id&#39;]!);\n      },\n    ),\n    GoRoute(\n      path: &#39;\/invite\/:code&#39;,\n      builder: (context, state) {\n        return InviteScreen(code: state.pathParameters[&#39;code&#39;]!);\n      },\n    ),\n  ],\n  errorBuilder: (context, state) =&gt; const NotFoundScreen(),\n);\n<\/code><\/pre>\n\n\n\n<p>With <code>flutter_deeplinking_enabled<\/code> set to <code>true<\/code>, the URL path is automatically forwarded to go_router.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">With app_links Package (For Full URL Access)<\/h3>\n\n\n\n<p>If you need access to the full URL (including the host), use the <code>app_links<\/code> package:<\/p>\n\n\n\n<pre><code class=\"language-dart\">import &#39;package:app_links\/app_links.dart&#39;;\n\nclass DeepLinkService {\n  final AppLinks _appLinks = AppLinks();\n\n  void init() {\n    _appLinks.uriLinkStream.listen((Uri uri) {\n      handleLink(uri);\n    });\n\n    _appLinks.getInitialLink().then((Uri? uri) {\n      if (uri != null) handleLink(uri);\n    });\n  }\n\n  void handleLink(Uri uri) {\n    \/\/ Access full URL: uri.host, uri.path, uri.queryParameters\n    router.go(uri.path);\n  }\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Step 4: Verify App Links<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">On-Device Verification<\/h3>\n\n\n\n<p>After installing your app, check that Android has verified the domain:<\/p>\n\n\n\n<pre><code class=\"language-bash\"># Check verification status\nadb shell pm get-app-links com.yourapp\n<\/code><\/pre>\n\n\n\n<p>The output shows the verification status for each domain:<\/p>\n\n\n\n<pre><code>com.yourapp:\n  ID: ...\n  Signatures: [...]\n  Domain verification state:\n    go.yourapp.com: verified\n<\/code><\/pre>\n\n\n\n<p>If it shows <code>none<\/code> or <code>legacy_failure<\/code>, the verification failed. Common causes:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The assetlinks.json is not accessible<\/li>\n<li>The SHA-256 fingerprint doesn&#39;t match<\/li>\n<li>The package name doesn&#39;t match<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Force Re-Verification<\/h3>\n\n\n\n<pre><code class=\"language-bash\"># Reset verification state\nadb shell pm set-app-links --package com.yourapp 0 all\n\n# Trigger re-verification\nadb shell pm verify-app-links --re-verify com.yourapp\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Test a Link<\/h3>\n\n\n\n<pre><code class=\"language-bash\">adb shell am start -a android.intent.action.VIEW \\\n  -d &quot;https:\/\/go.yourapp.com\/product\/123&quot; \\\n  com.yourapp\n<\/code><\/pre>\n\n\n\n<p>If the app opens directly (no disambiguation dialog), App Links are working.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Troubleshooting<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Disambiguation Dialog Still Appears<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Verify <code>android:autoVerify=&quot;true&quot;<\/code> is on the intent filter<\/li>\n<li>Check that assetlinks.json is accessible over HTTPS without redirects<\/li>\n<li>Confirm the SHA-256 fingerprint matches (especially the Google Play App Signing key)<\/li>\n<li>Try resetting and re-verifying via ADB<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Links Open in Browser<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The app may not be set as the default handler. Go to Settings &gt; Apps &gt; Your App &gt; Open by default &gt; Supported links, and enable it.<\/li>\n<li>Some Android manufacturers have custom link handling that ignores App Links. Samsung and Xiaomi devices are known to have quirks.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Links Work in ADB but Not from Browser<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Chrome handles App Links differently than ADB. Verify the intent filter is correctly configured.<\/li>\n<li>Some browsers intercept links instead of delegating to the OS. Test from multiple apps (Gmail, Messages, etc.).<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Debug vs Release Fingerprint Mismatch<\/h3>\n\n\n\n<p>The most common issue: you test with a debug build (which uses the debug keystore) but your assetlinks.json only has the release fingerprint. Include both fingerprints during development.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Production Checklist<\/h2>\n\n\n\n<ul class=\"checklist wp-block-list\"><li><input type=\"checkbox\" disabled> Intent filter with <code>autoVerify=\"true\"<\/code> in AndroidManifest.xml<\/li><li><input type=\"checkbox\" disabled> <code>flutter_deeplinking_enabled<\/code> metadata set to <code>true<\/code><\/li><li><input type=\"checkbox\" disabled> <code>launchMode=\"singleTop\"<\/code> on the activity<\/li><li><input type=\"checkbox\" disabled> assetlinks.json accessible at <code>\/.well-known\/assetlinks.json<\/code><\/li><li><input type=\"checkbox\" disabled> SHA-256 fingerprint includes release key<\/li><li><input type=\"checkbox\" disabled> SHA-256 fingerprint includes Google Play App Signing key (if applicable)<\/li><li><input type=\"checkbox\" disabled> Domain verification status is &#8220;verified&#8221; via <code>adb shell pm get-app-links<\/code><\/li><li><input type=\"checkbox\" disabled> Links open without disambiguation dialog on physical device<\/li><li><input type=\"checkbox\" disabled> Cold start and warm start links both work<\/li><li><input type=\"checkbox\" disabled> go_router routes match all deep link paths<\/li><\/ul>\n\n\n\n<p>For the cross-platform perspective, see <a href=\"https:\/\/tolinku.com\/blog\/cross-platform-deep-linking-guide\/\">Cross-Platform Deep Linking Guide<\/a>. For the Tolinku Flutter SDK, see the <a href=\"https:\/\/tolinku.com\/docs\/developer\/sdks\/flutter\/\">Flutter SDK docs<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Set up Android App Links for Flutter apps. Configure intent filters, verify Digital Asset Links, and handle deep link routing in Dart.<\/p>\n","protected":false},"author":2,"featured_media":901,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"App Links in Flutter: Android Configuration Guide","rank_math_description":"Set up Android App Links for Flutter apps. Configure intent filters, verify Digital Asset Links, and handle deep link routing in Dart.","rank_math_focus_keyword":"Flutter app links","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-flutter-app-links.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-flutter-app-links.png","footnotes":""},"categories":[15],"tags":[25,23,179,20,35,57,184,69],"class_list":["post-902","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-engineering","tag-android","tag-app-links","tag-dart","tag-deep-linking","tag-digital-asset-links","tag-flutter","tag-intent-filters","tag-mobile-development"],"_links":{"self":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/902","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=902"}],"version-history":[{"count":1,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/902\/revisions"}],"predecessor-version":[{"id":903,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/902\/revisions\/903"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media\/901"}],"wp:attachment":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media?parent=902"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/categories?post=902"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/tags?post=902"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}