Skip to content
Blog

Android App Links in Flutter: Verified Deep Links End to End

End-to-end Android App Links for Flutter: intent-filter anatomy, assetlinks.json hosting, adb verification commands, Play Console status, and failure modes.

Published on • November 12, 2026

AI Assistant

Android App Links turn an ordinary https URL into a link that opens your Flutter app directly, with no chooser dialog, because the OS has cryptographically confirmed your app owns both the domain and the intent handler. The combined tutorial in Deep Linking and App Links in 2026 covered the outline of this flow; here is the Android half in detail: what each manifest attribute actually does, how to host the association file correctly, how to prove verification happened, and why links silently fall back to the browser.

Intent filter anatomy

Everything hangs off one <intent-filter> on the activity that receives the link, which for a Flutter app is .MainActivity:

<activity
    android:name=".MainActivity"
    android:exported="true"
    android:launchMode="singleTop"
    android:taskAffinity="">
    <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="example.com" android:pathPrefix="/p" />
    </intent-filter>
</activity>
AttributeValueWhy it matters
android:autoVerifytrueTriggers automatic domain verification on install, Android 6.0 (API 23) and higher
actionVIEWOne of the three markers the verification agent looks for
categoryBROWSABLE and DEFAULTLets a browser start the activity; DEFAULT is required for implicit intents to resolve at all
datahttps + hostVerification only queries https, so an http-only filter never verifies
android:exportedtrueRequired explicitly: an activity with intent filters and no android:exported value cannot be installed on Android 12 or higher
android:launchModesingleTopMatches Flutter’s own app template, so a second tap delivers onNewIntent instead of stacking another activity

Launch mode interacts with deep links more than people expect. Flutter’s default template ships android:launchMode="singleTop" and an empty android:taskAffinity, so with an existing instance at the top of the task the new URL arrives through onNewIntent and your running engine handles it. Under the default standard mode every tap creates a fresh MainActivity, which produces a stack of Flutter engines and a back button that walks through duplicate screens. singleTask, singleInstance, and singleInstancePerTask are documented as not appropriate for most applications, because they relocate the activity to a task root and surprise users who expect a normal back stack. Keep web links for one path and activity inside a single intent filter, because Play Console warns that splitting them across multiple <data> tags is dangerous when the system combines every scheme, host, and path permutation.

Hosting assetlinks.json correctly

Verification fetches https://<hostname>/.well-known/assetlinks.json for every unique host in your filters:

[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.example.myapp",
    "sha256_cert_fingerprints": [
      "14:6D:E9:83:C5:73:06:50:D8:EE:B9:95:2F:34:FC:64:AD:A3:48:B1:A6:14:1C:2D:44:4E"
    ]
  }
}]

The rules around that file are strict:

  • Served over HTTPS with content type application/json, regardless of the scheme your intent filter declares.
  • No redirects at all, including 301 and 302, at any point in the path.
  • Published on every host you declare, so adding www.example.com to the filter means a second file on that host.
  • package_name is the applicationId from build.gradle, not the manifest package.
  • sha256_cert_fingerprints comes from keytool -list -v -keystore <path> and can hold several fingerprints, which is how debug and production builds coexist.
  • With Play App Signing, the local keystore fingerprint usually does not match what devices see. Copy the snippet Play Console gives you under Release > Setup > App integrity instead.
  • Never ship a filter pointing at a VPN-only or localhost test host.

Running verification

Install the build, then give the verifier time. Android’s own guidance is to wait at least 20 seconds for the asynchronous verification to finish before checking anything:

# Reset, re-verify, and read the result (Android 12+)
adb shell pm set-app-links --package com.example.myapp 0 all
adb shell pm verify-app-links --re-verify com.example.myapp
sleep 60
adb shell pm get-app-links com.example.myapp

# Apps targeting below Android 12 need the updated process enabled first
adb shell am compat enable 175408749 com.example.myapp

# Fire an actual link at the installed app
adb shell am start -a android.intent.action.VIEW \
    -c android.intent.category.BROWSABLE \
    -d "https://example.com/p/42" com.example.myapp

A healthy pm get-app-links output ends with a line per host:

com.example.myapp:
    ID: 01234567-89ab-cdef-0123-456789abcdef
    Signatures: [***]
    Domain verification state:
      example.com: verified
      www.example.com: legacy_failure
      example.org: 1026
StateMeaning
verifiedThe domain passed, links open the app directly
noneNothing recorded yet, the verifier has not finished, retry in a few minutes
approved / deniedForced by a shell command or by the user in settings
legacy_failureRejected by the older verifier, reason unknown
1024 and aboveVerifier-specific error code, usually a network problem

Verified versus unverified behavior

Once a domain is verified, tapping a matching link opens the app with no dialog. Without verification the URL behaves like an ordinary deep link: the system shows a disambiguation chooser or sends the user to the browser, and on Android 11 and lower the app becomes the default handler only if the association file matched every host in the manifest, so one broken www host takes down the whole set. Only one app can be associated with a domain at a time on a given device, and users can flip the association themselves in Settings > Apps > Open by default, which is exactly what pm get-app-links --user cur <package> reports alongside the verification states.

Verification results also age slowly. Android 15 and higher re-verify in the background and can take up to seven days to pick up a changed assetlinks.json; Android 14 and lower normally re-read it only on install or update. Server-side caches can hold the old file for hours even after a reinstall.

Play Console exposes the same data as a dashboard under Grow users > Deep links. The App configuration tab lists every domain from your manifest with an OK or Not OK status you can hover for the exact failed check, plus a manifest links table that validates your intent filters per path. The Web URLs tab shows which crawled and ad-driven domains actually deep link, with a user reach breakdown of app, app picker, and browser outcomes. Patches let you add, turn off, or fix a deep link for active versions without shipping a new release, although manifest fixes still require one. Note the console’s own caveat: fixing the file does not instantly convert installed users, because pre-Android 12 verification happens at install time.

Failure modes

SymptomLikely causeFix
State stays none foreverFile at the wrong path, served over HTTP, or behind a redirectServe https://host/.well-known/assetlinks.json directly with application/json
State legacy_failure or 1026Package name or SHA-256 mismatch, often Play App SigningMatch applicationId and paste the Play Console fingerprint
One host verifies, another does notOnly published on the apex domainPublish the file on every host in the filter, or drop the extra host
Link opens the chooser every timeautoVerify missing, or filter lacks BROWSABLEAdd android:autoVerify="true" and both categories
Link works from adb but not a browserYou tested intent delivery, not the hosted fileTap a real URL from a browser or another app

Takeaway: App Links are verified end to end or they are not links at all, so declare one explicit android:exported and autoVerify filter on a singleTop activity, publish a redirect-free assetlinks.json on every host with the fingerprint your users actually run, then prove it with pm verify-app-links --re-verify and a real browser tap before you trust a campaign URL.

Sources