Skip to content
Tolinku
Tolinku
Sign In Start Free
Engineering · · 6 min read

App Links in Flutter: Android Configuration Guide

By Tolinku Staff
|
Tolinku cross platform dashboard screenshot for engineering blog posts

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.

For the full Flutter deep linking setup (including iOS), see Flutter Deep Linking: Complete Setup Tutorial. For go_router integration, see Flutter go_router Deep Links. For the Android fundamentals, see Android App Links: Complete Implementation Guide.

Android has two types of link handling:

Deep Links (basic): Use intent-filter without autoVerify. 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.

App Links (verified): Use intent-filter with android:autoVerify="true". Android verifies that you own the domain by checking /.well-known/assetlinks.json. Once verified, your app opens directly with no dialog.

Always use App Links for production. Deep Links without verification are only appropriate for development and testing.

Step 1: Configure AndroidManifest.xml

Open android/app/src/main/AndroidManifest.xml and add intent filters to your main activity:

<activity
  android:name=".MainActivity"
  android:exported="true"
  android:launchMode="singleTop"
  android:theme="@style/LaunchTheme"
  android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
  android:hardwareAccelerated="true"
  android:windowSoftInputMode="adjustResize">

  <!-- Flutter deep linking metadata -->
  <meta-data
    android:name="flutter_deeplinking_enabled"
    android:value="true" />

  <!-- Default Flutter intent filter -->
  <intent-filter>
    <action android:name="android.intent.action.MAIN"/>
    <category android:name="android.intent.category.LAUNCHER"/>
  </intent-filter>

  <!-- App Links (verified) -->
  <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="go.yourapp.com" />
  </intent-filter>
</activity>

Key Attributes

  • android:autoVerify="true": Triggers Android's domain verification process. Without this, links show a disambiguation dialog.
  • android:launchMode="singleTop": 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.
  • flutter_deeplinking_enabled: Tells Flutter's engine to forward incoming URLs to your Dart router automatically.

Multiple Domains

If your links come from multiple domains:

<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="go.yourapp.com" />
  <data android:scheme="https" android:host="links.yourapp.com" />
</intent-filter>

Note: each domain you add requires its own assetlinks.json file.

Path Restrictions

To handle only specific paths (not all URLs on the domain):

<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="go.yourapp.com"
    android:pathPrefix="/product" />
  <data
    android:scheme="https"
    android:host="go.yourapp.com"
    android:pathPrefix="/invite" />
</intent-filter>

Available path matching options:

  • android:path="/exact/path" – Exact match
  • android:pathPrefix="/prefix" – Starts with
  • android:pathPattern="/product/.*" – Regex pattern

Your domain must serve a verification file at:

https://go.yourapp.com/.well-known/assetlinks.json

File Content

[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.yourapp",
    "sha256_cert_fingerprints": [
      "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"
    ]
  }
}]

If using Tolinku, the assetlinks.json is generated automatically when you configure your Appspace with your package name and SHA-256 fingerprints.

Getting Your SHA-256 Fingerprint

Debug keystore:

keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android

Release keystore:

keytool -list -v -keystore /path/to/your/release.keystore -alias your-alias

Google Play App Signing: If you use Google Play's app signing feature (recommended), Google re-signs your APK with their own key. You need both:

  1. Your upload key fingerprint (from your keystore)
  2. Google's app signing key fingerprint (from Play Console: Setup > App signing > App signing key certificate)

Include both in your assetlinks.json:

[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.yourapp",
    "sha256_cert_fingerprints": [
      "YOUR:UPLOAD:KEY:FINGERPRINT",
      "GOOGLE:SIGNING:KEY:FINGERPRINT"
    ]
  }
}]

Verification

Check that the file is accessible and valid:

# Accessibility
curl -I https://go.yourapp.com/.well-known/assetlinks.json

# Content
curl https://go.yourapp.com/.well-known/assetlinks.json | python3 -m json.tool

Use Google's Digital Asset Links tool to validate the file against your app.

final router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomeScreen(),
    ),
    GoRoute(
      path: '/product/:id',
      builder: (context, state) {
        return ProductScreen(id: state.pathParameters['id']!);
      },
    ),
    GoRoute(
      path: '/invite/:code',
      builder: (context, state) {
        return InviteScreen(code: state.pathParameters['code']!);
      },
    ),
  ],
  errorBuilder: (context, state) => const NotFoundScreen(),
);

With flutter_deeplinking_enabled set to true, the URL path is automatically forwarded to go_router.

If you need access to the full URL (including the host), use the app_links package:

import 'package:app_links/app_links.dart';

class DeepLinkService {
  final AppLinks _appLinks = AppLinks();

  void init() {
    _appLinks.uriLinkStream.listen((Uri uri) {
      handleLink(uri);
    });

    _appLinks.getInitialLink().then((Uri? uri) {
      if (uri != null) handleLink(uri);
    });
  }

  void handleLink(Uri uri) {
    // Access full URL: uri.host, uri.path, uri.queryParameters
    router.go(uri.path);
  }
}

On-Device Verification

After installing your app, check that Android has verified the domain:

# Check verification status
adb shell pm get-app-links com.yourapp

The output shows the verification status for each domain:

com.yourapp:
  ID: ...
  Signatures: [...]
  Domain verification state:
    go.yourapp.com: verified

If it shows none or legacy_failure, the verification failed. Common causes:

  • The assetlinks.json is not accessible
  • The SHA-256 fingerprint doesn't match
  • The package name doesn't match

Force Re-Verification

# Reset verification state
adb shell pm set-app-links --package com.yourapp 0 all

# Trigger re-verification
adb shell pm verify-app-links --re-verify com.yourapp
adb shell am start -a android.intent.action.VIEW \
  -d "https://go.yourapp.com/product/123" \
  com.yourapp

If the app opens directly (no disambiguation dialog), App Links are working.

Troubleshooting

Disambiguation Dialog Still Appears

  • Verify android:autoVerify="true" is on the intent filter
  • Check that assetlinks.json is accessible over HTTPS without redirects
  • Confirm the SHA-256 fingerprint matches (especially the Google Play App Signing key)
  • Try resetting and re-verifying via ADB
  • The app may not be set as the default handler. Go to Settings > Apps > Your App > Open by default > Supported links, and enable it.
  • Some Android manufacturers have custom link handling that ignores App Links. Samsung and Xiaomi devices are known to have quirks.
  • Chrome handles App Links differently than ADB. Verify the intent filter is correctly configured.
  • Some browsers intercept links instead of delegating to the OS. Test from multiple apps (Gmail, Messages, etc.).

Debug vs Release Fingerprint Mismatch

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.

Production Checklist

  • Intent filter with autoVerify="true" in AndroidManifest.xml
  • flutter_deeplinking_enabled metadata set to true
  • launchMode="singleTop" on the activity
  • assetlinks.json accessible at /.well-known/assetlinks.json
  • SHA-256 fingerprint includes release key
  • SHA-256 fingerprint includes Google Play App Signing key (if applicable)
  • Domain verification status is “verified” via adb shell pm get-app-links
  • Links open without disambiguation dialog on physical device
  • Cold start and warm start links both work
  • go_router routes match all deep link paths

For the cross-platform perspective, see Cross-Platform Deep Linking Guide. For the Tolinku Flutter SDK, see the Flutter SDK docs.

Get deep linking tips in your inbox

One email per week. No spam.

Ready to add deep linking to your app?

Set up Universal Links, App Links, deferred deep linking, and analytics in minutes. Free to start.