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>
| Attribute | Value | Why it matters |
|---|---|---|
android:autoVerify | true | Triggers automatic domain verification on install, Android 6.0 (API 23) and higher |
action | VIEW | One of the three markers the verification agent looks for |
category | BROWSABLE and DEFAULT | Lets a browser start the activity; DEFAULT is required for implicit intents to resolve at all |
data | https + host | Verification only queries https, so an http-only filter never verifies |
android:exported | true | Required explicitly: an activity with intent filters and no android:exported value cannot be installed on Android 12 or higher |
android:launchMode | singleTop | Matches 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.comto the filter means a second file on that host. package_nameis theapplicationIdfrombuild.gradle, not the manifest package.sha256_cert_fingerprintscomes fromkeytool -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
| State | Meaning |
|---|---|
verified | The domain passed, links open the app directly |
none | Nothing recorded yet, the verifier has not finished, retry in a few minutes |
approved / denied | Forced by a shell command or by the user in settings |
legacy_failure | Rejected by the older verifier, reason unknown |
1024 and above | Verifier-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’s Deep links page
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
| Symptom | Likely cause | Fix |
|---|---|---|
State stays none forever | File at the wrong path, served over HTTP, or behind a redirect | Serve https://host/.well-known/assetlinks.json directly with application/json |
State legacy_failure or 1026 | Package name or SHA-256 mismatch, often Play App Signing | Match applicationId and paste the Play Console fingerprint |
| One host verifies, another does not | Only published on the apex domain | Publish the file on every host in the filter, or drop the extra host |
| Link opens the chooser every time | autoVerify missing, or filter lacks BROWSABLE | Add android:autoVerify="true" and both categories |
Link works from adb but not a browser | You tested intent delivery, not the hosted file | Tap 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
- https://developer.android.com/training/app-links
- https://developer.android.com/training/app-links/configure-assetlinks
- https://developer.android.com/training/app-links/verify-applinks
- https://developer.android.com/guide/topics/manifest/activity-element
- https://support.google.com/googleplay/android-developer/answer/12463044
- https://docs.flutter.dev/cookbook/navigation/set-up-app-links