skip to content

In a Flutter app, what do you configure on Android and iOS so shared https event links open the app instead of the browser?

level: middleimportance: must knowfreq 55%

answer

  1. domain and app vouch for each other
  2. autoVerify intent filter in AndroidManifest
  3. /.well-known/assetlinks.json with the cert fingerprint
  4. applinks: entitlement plus apple-app-site-association
  5. Flutter hands the URL to the router

basics

~20 s

On Android, add an intent filter with android:autoVerify="true" for the https host and serve /.well-known/assetlinks.json with the package name and signing fingerprint; on iOS, add the applinks: associated domain and serve apple-app-site-association; Flutter then passes the URL to the router.

solid answer

~40 s

An https link opens an app only when the **domain vouches for the app**. On Android I add an intent filter to the `MainActivity` in `AndroidManifest.xml` with `android:autoVerify="true"`, the `VIEW` action, the `DEFAULT` and `BROWSABLE` categories and `<data>` for the `https` scheme and the host; the site serves `https://<host>/.well-known/assetlinks.json` naming the package and the SHA-256 fingerprint of the signing certificate. On iOS I enable the Associated Domains capability with `applinks:<host>` in `Runner.entitlements`, and the site serves `/.well-known/apple-app-site-association` (no extension) with `<team id>.<bundle id>` in `appIDs` and the allowed paths. Since Flutter 3.27 the deep-linking flag is on by default, so the engine hands the URL to the app's `Router`, and go_router maps `/events/42` to the event page.

code

xml · 7 lines
xml
<!-- android/app/src/main/AndroidManifest.xml, inside <activity android:name=".MainActivity"> -->
<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="tickets.example.com" android:pathPrefix="/events" />
</intent-filter>

go deeper

for a junior

Recall the pairs: an autoVerify intent filter with assetlinks.json on Android, and the applinks: associated domain with apple-app-site-association on iOS.

for a middle

Explain what each file proves, which identifiers they carry, how Flutter forwards the URL to the Router, and that go_router matches the path only.

for a senior

Show you get signing fingerprints, flavors and path scopes right for production, and validate the setup with DevTools before shipping links in campaigns.

for a principal

Plan link ownership across web and app teams: who maintains the hosted files, which paths open the app, and how links degrade when the app is absent.

## Why verification exists An ordinary `https://tickets.example.com/events/42` link belongs to a website. For the operating system to open a **ticketing app** instead, it needs proof that the owner of `tickets.example.com` wants that specific app to handle its URLs. Both platforms get that proof the same way: the app declares the domain, and the domain publishes a file naming the app. Android calls the result **App Links**; iOS calls it **Universal Links**. Once verified, a tap on a shared event link opens the app straight on the event page, and when the app is not installed the same link still works as a web page. ## Android: App Links 1. In `android/app/src/main/AndroidManifest.xml`, inside the `<activity>` for `.MainActivity`, add an intent filter with `android:autoVerify="true"`, the `android.intent.action.VIEW` action, the `DEFAULT` and `BROWSABLE` categories, and `<data>` elements for the `https` scheme and `tickets.example.com` host. 2. Host `assetlinks.json` at `https://tickets.example.com/.well-known/assetlinks.json`. Its entry has the relation `delegate_permission/common.handle_all_urls` and a `target` with `namespace` `android_app`, the `package_name` (the application ID) and `sha256_cert_fingerprints`. 3. Use the fingerprint of the certificate that **signs the installed build**. With Play App Signing that is the app signing key shown in the Play Console, not the local upload key; `keytool -list -v -keystore <path>` prints a local key's fingerprint. Several flavors or keys can be listed in the same array. ## iOS: Universal Links 1. In Xcode, open `ios/Runner.xcworkspace`, select the Runner target, add the **Associated Domains** capability, and add `applinks:tickets.example.com`. This writes `com.apple.developer.associated-domains` into `ios/Runner/Runner.entitlements`. Personal development teams do not support this capability. 2. Host a JSON file named `apple-app-site-association`, **without** a `.json` extension, at `https://tickets.example.com/.well-known/apple-app-site-association`. 3. In it, `applinks.details` lists `appIDs` in the form `<team id>.<bundle id>` and `components` describing the paths the app handles, for example `/events/*` rather than every path on the site. | | Android App Links | iOS Universal Links | |---|---|---| | App side | `autoVerify` intent filter in `AndroidManifest.xml` | `applinks:` entry in Associated Domains | | Site file | `/.well-known/assetlinks.json` | `/.well-known/apple-app-site-association` | | Identifies the app by | package name plus certificate SHA-256 | team ID plus bundle ID | | Path filter lives in | the intent filter's `<data>` | the file's `components` | ## Flutter's part The Flutter engine forwards the incoming URL to the framework when its **deep-linking flag** is enabled, which is the default since Flutter 3.27 (`flutter_deeplinking_enabled` meta-data on Android, `FlutterDeepLinkingEnabled` in `Info.plist` on iOS). On Android, the intent's data URI becomes the initial route on a cold start and is sent as a `pushRouteInformation` message when the app is already running. On iOS, the URL arrives through the app or scene delegate and is sent to the framework the same way. The app then needs a **`Router`**, usually go_router, to turn the location into screens. go_router matches only the URI's **path**, so `https://tickets.example.com/events/42` is matched as `/events/42`, the same route the web build uses. Mapping paths to pages, redirects for signed-out users and error screens belong to the router configuration. ## Flavors, several domains and common slips - **Flavors**: a staging build with the application ID `com.example.tickets.staging` needs its own entry in `assetlinks.json`, and a separate bundle ID needs its own `appIDs` entry; both files accept several apps. - **Several hosts**: `tickets.example.com` and `www.tickets.example.com` are different hosts. Each needs its own `<data>` host or `applinks:` entry and its own hosted file. - **Signing**: debug builds, locally signed release builds and store installs can be signed with three different keys; list every fingerprint you expect users or testers to have. - **Scope**: narrow both sides to the paths the app can show (`pathPrefix="/events"`, `components` of `/events/*`), so account, checkout or help pages keep opening on the web. - **Leftovers**: the opt-in `flutter_deeplinking_enabled` or `FlutterDeepLinkingEnabled` entries with `true` from older tutorials are redundant since 3.27; an old entry with `false` silently disables everything. ## Checking the result - DevTools has a **Deep Links** tab that validates both the manifest or entitlements and the hosted files, for Android and, since Flutter 3.27, iOS. - `adb shell am start -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "https://tickets.example.com/events/42" <package>` tests the Android app side only. - `xcrun simctl openurl booted https://tickets.example.com/events/42` tests a Simulator.

  • With Android App Links for a Flutter app, which certificate fingerprint belongs in assetlinks.json when the app ships through Play App Signing?
    The SHA-256 of the app signing key that Google Play uses, shown in the Play Console under the app signing settings, because that key signs what users install. The local upload key's fingerprint only matches builds you sign yourself; listing both keeps local release builds and store installs verified.
  • With iOS Universal Links, why should the apple-app-site-association components list specific paths?
    The file decides which URLs on the domain open the app. Matching `/*` also sends the marketing pages, help centre and checkout to the app, which may have no screen for them. Listing `/events/*` keeps everything else on the website.
  • In a Flutter app using go_router, which part of https://tickets.example.com/events/42?seat=A7 does routing match?
    Only the path, `/events/42`. go_router ignores the scheme and host when matching, and the query string arrives in `state.uri.queryParameters`, so `seat` is read there, not declared in the route.

saying these in an interview costs you the question

  • Believes an https intent filter alone opens the app without a hosted assetlinks.json.
  • Puts the upload key's fingerprint in assetlinks.json for a Play App Signing app.
  • Names the iOS file apple-app-site-association.json.
  • Thinks Flutter's deep-linking flag must still be switched on by hand in 3.47.
  • Expects the link to reach a screen without any Router or route for its path.