Back to Blog
Implementation
9 min read
February 15, 2024

How to Set Up Android App Links in Flutter

Master Android App Links with this comprehensive guide. Learn AndroidManifest.xml configuration, assetlinks.json setup, SHA-256 generation, and Dart implementation.

What are Android App Links?

Definition

Android App Links are a special type of deep link that use standard HTTPS URLs to launch your app directly without showing the app chooser dialog. They verify the link belongs to the app domain using the assetlinks.json file hosted on your server.

Key Advantages

  • • No app chooser dialog
  • • Direct app launching
  • • Domain verification
  • • Seamless UX
  • • Better engagement
  • • Works with Chrome/browsers

How They Work

  • • User clicks an HTTPS link
  • • Android checks intent filters
  • • Android validates assetlinks.json
  • • App opens if verified
  • • Domain matches SHA-256
  • • No fallback dialog shown

AndroidManifest.xml Configuration

Step 1: Add Intent Filters

Update your MainActivity in AndroidManifest.xml to handle App Links:

xml
<activity android:name=".MainActivity"
    android:launchMode="singleTask">

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

    <!-- App Links intent filter -->
    <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="yourdomain.com"
            android:path="/product/*" />
        <data
            android:scheme="https"
            android:host="yourdomain.com"
            android:path="/user/*" />
        <data
            android:scheme="https"
            android:host="yourdomain.com"
            android:path="/share/*" />
    </intent-filter>

</activity>

Important: The android:autoVerify="true" attribute tells Android to verify this is a valid App Link.

Step 2: Configure launchMode

Set launchMode to singleTask to prevent multiple instances when app links are opened:

xml
android:launchMode="singleTask"

Generate SHA-256 Fingerprints

What is SHA-256?

A SHA-256 fingerprint is a cryptographic hash of your app's signing certificate. Android uses it to verify that assetlinks.json matches your app.

Step 1: Generate Debug SHA-256

Use keytool to get the SHA-256 of your debug keystore:

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

On macOS/Linux. For Windows, replace ~/.android with your keystore path.

Step 2: Generate Release SHA-256

For your release keystore (replace paths with your keystore file):

bash
keytool -list -v -keystore /path/to/your/keystore.jks -alias your_key_alias -storepass your_store_password -keypass your_key_password

Output Format

Look for the line starting with "SHA256:". It will look like: 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

Dart Code Implementation

Step 1: Add uni_links Package

Add the uni_links package to your pubspec.yaml:

yaml
dependencies:
  flutter:
    sdk: flutter
  uni_links: ^0.9.0

Step 2: Create App Link Handler

Implement the handler for incoming App Links:

dart
import 'package:flutter/material.dart';
import 'package:uni_links/uni_links.dart';
import 'dart:async';

class AppLinkHandler {
  static StreamSubscription? _deepLinkSubscription;

  static Future<void> initialize(BuildContext context) async {
    // Handle link when app is opened from terminated state
    final String? initialLink = await getInitialLink();
    if (initialLink != null) {
      _handleAppLink(initialLink, context);
    }

    // Listen for app links when app is running
    _deepLinkSubscription = linkStream.listen(
      (String link) {
        _handleAppLink(link, context);
      },
      onError: (err) {
        debugPrint('App link error: $err');
      },
    );
  }

  static void _handleAppLink(String link, BuildContext context) {
    final Uri uri = Uri.parse(link);

    debugPrint('Received app link: $link');
    debugPrint('Scheme: ${uri.scheme}');
    debugPrint('Host: ${uri.host}');
    debugPrint('Path: ${uri.path}');
    debugPrint('Query parameters: ${uri.queryParameters}');

    // Route based on path segments
    if (uri.pathSegments.isNotEmpty) {
      final String firstSegment = uri.pathSegments[0];

      switch (firstSegment) {
        case 'product':
          if (uri.pathSegments.length > 1) {
            final String productId = uri.pathSegments[1];
            _navigateToProduct(context, productId);
          }
          break;
        case 'user':
          if (uri.pathSegments.length > 1) {
            final String userId = uri.pathSegments[1];
            _navigateToUser(context, userId);
          }
          break;
        case 'share':
          if (uri.pathSegments.length > 1) {
            final String referralCode = uri.pathSegments[1];
            _handleReferral(context, referralCode);
          }
          break;
        default:
          _navigateToHome(context);
      }
    } else {
      _navigateToHome(context);
    }
  }

  static void _navigateToProduct(BuildContext context, String productId) {
    debugPrint('Navigate to product: $productId');
    // Implementation: Navigate to product screen
  }

  static void _navigateToUser(BuildContext context, String userId) {
    debugPrint('Navigate to user: $userId');
    // Implementation: Navigate to user profile screen
  }

  static void _handleReferral(BuildContext context, String referralCode) {
    debugPrint('Handle referral: $referralCode');
    // Implementation: Handle referral code
  }

  static void _navigateToHome(BuildContext context) {
    debugPrint('Navigate to home');
    // Implementation: Navigate to home screen
  }

  static void dispose() {
    _deepLinkSubscription?.cancel();
  }
}

Step 3: Initialize in main.dart

Initialize the handler in your main app widget:

dart
import 'package:flutter/material.dart';
import 'app_link_handler.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      AppLinkHandler.initialize(context);
    });
  }

  @override
  void dispose() {
    AppLinkHandler.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My App',
      home: const HomeScreen(),
    );
  }
}

Testing App Links

Test with adb (Android Debug Bridge)

Use ADB to simulate app link clicks:

bash
adb shell am start -W -a android.intent.action.VIEW -d "https://yourdomain.com/product/123" com.example.myapp

Manual Testing on Device

Test app links manually on your Android device:

1. Open Chrome or another browser on your device

2. Navigate to your app link URL: https://yourdomain.com/product/123

3. App should open directly without app chooser dialog

4. Verify correct screen/content is displayed

Verify assetlinks.json is Live

Check if your assetlinks.json is accessible:

bash
curl -I https://yourdomain.com/.well-known/assetlinks.json

Expected response: HTTP/1.1 200 OK

Common Issues & Fixes

Issue: assetlinks.json Not Found (404)

Cause: File not uploaded or wrong path

Solution:

  • • Verify file is at /.well-known/assetlinks.json
  • • Check file is properly uploaded to server
  • • Test with curl: curl https://yourdomain.com/.well-known/assetlinks.json
  • • Validate using our validator

Issue: SHA-256 Fingerprint Mismatch

Cause: Wrong SHA-256 in assetlinks.json

Solution:

  • • Regenerate SHA-256 using keytool
  • • Ensure fingerprint matches your signing certificate
  • • Check for typos in the hash
  • • Include both debug and release hashes if testing both

Issue: Deep Link Opens Browser Instead of App

Cause: Intent filter or autoVerify configuration missing

Solution:

  • • Verify android:autoVerify="true" is set
  • • Check intent filter has correct data entries
  • • Rebuild and reinstall app
  • • Wait a few seconds after install before testing

Issue: App Not Receiving Deep Link

Cause: Handler not initialized or context issue

Solution:

  • • Initialize handler after widget tree is built
  • • Use WidgetsBinding.addPostFrameCallback()
  • • Check debug output for handler logs
  • • Ensure uni_links is in pubspec.yaml

Master Deep Linking with go_router

Now that you have App Links working, learn how to use go_router for advanced deep linking with route guards and deferred links.

Related Articles