Master Android App Links with this comprehensive guide. Learn AndroidManifest.xml configuration, assetlinks.json setup, SHA-256 generation, and Dart implementation.
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.
Update your MainActivity in AndroidManifest.xml to handle App Links:
<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.
Set launchMode to singleTask to prevent multiple instances when app links are opened:
android:launchMode="singleTask"
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.
Use keytool to get the SHA-256 of your debug keystore:
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
On macOS/Linux. For Windows, replace ~/.android with your keystore path.
For your release keystore (replace paths with your keystore file):
keytool -list -v -keystore /path/to/your/keystore.jks -alias your_key_alias -storepass your_store_password -keypass your_key_password
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
The assetlinks.json file proves you own both the domain and the app. It must be hosted at /.well-known/assetlinks.json on your domain.
Create the JSON file with your app's package name and SHA-256 fingerprints:
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example.myapp",
"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"
]
}
}
]Include both debug and release SHA-256 fingerprints if you test with both:
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example.myapp",
"sha256_cert_fingerprints": [
"DEBUG: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:AA:BB",
"RELEASE: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"
]
}
}
]Upload the file to your domain's .well-known directory:
https://yourdomain.com/.well-known/assetlinks.json
Tip: Validate your assetlinks.json using our assetlinks Validator tool.
Add the uni_links package to your pubspec.yaml:
dependencies:
flutter:
sdk: flutter
uni_links: ^0.9.0Implement the handler for incoming App Links:
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();
}
}Initialize the handler in your main app widget:
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(),
);
}
}Use ADB to simulate app link clicks:
adb shell am start -W -a android.intent.action.VIEW -d "https://yourdomain.com/product/123" com.example.myapp
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
Check if your assetlinks.json is accessible:
curl -I https://yourdomain.com/.well-known/assetlinks.json
Expected response: HTTP/1.1 200 OK
Cause: File not uploaded or wrong path
Solution:
/.well-known/assetlinks.jsoncurl https://yourdomain.com/.well-known/assetlinks.jsonCause: Wrong SHA-256 in assetlinks.json
Solution:
Cause: Intent filter or autoVerify configuration missing
Solution:
android:autoVerify="true" is setCause: Handler not initialized or context issue
Solution:
Now that you have App Links working, learn how to use go_router for advanced deep linking with route guards and deferred links.