Flutter Deep Linking
Open specific app screens from external links
🔗 What is Deep Linking?
Deep Linking allows users to open specific screens in your Flutter app directly from external sources like websites, emails, or other apps. It enables seamless navigation from web URLs to app content, improving user experience and enabling features like sharing, notifications, and marketing campaigns.
// Handle incoming deep link
myapp://product/123
Key Deep Linking Concepts
URL Schemes
Custom app URL patterns
myapp://screen/id
Universal Links
Web URLs that open app
https://myapp.com/product/123
App Links
Android verified links
android:autoVerify="true"
Route Handling
Parse and navigate to screens
onGenerateRoute: (settings) {
// Parse URL
}
🔹 Basic Deep Link Setup
Configure your app to handle deep links:
1. Add dependency to pubspec.yaml:
dependencies:
flutter:
sdk: flutter
uni_links: ^0.5.1 # For handling deep links
2. Android Configuration (AndroidManifest.xml):
<activity
android:name=".MainActivity">
<!-- Deep Link Intent Filter -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Custom URL scheme -->
<data
android:scheme="myapp"
android:host="product" />
</intent-filter>
</activity>
3. iOS Configuration (Info.plist):
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>
URL Format:
With this setup, your app can handle URLs like:
myapp://product/123
🔹 Handling Deep Links in Flutter
Listen for and process incoming deep links:
import 'package:flutter/material.dart';
import 'package:uni_links/uni_links.dart';
import 'dart:async';
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State {
StreamSubscription? _linkSubscription;
@override
void initState() {
super.initState();
_initDeepLinks();
}
Future _initDeepLinks() async {
// Handle initial link when app is opened from closed state
try {
final initialLink = await getInitialLink();
if (initialLink != null) {
_handleDeepLink(initialLink);
}
} catch (e) {
print('Error getting initial link: $e');
}
// Listen for links when app is already running
_linkSubscription = linkStream.listen((String? link) {
if (link != null) {
_handleDeepLink(link);
}
}, onError: (err) {
print('Error listening to links: $err');
});
}
void _handleDeepLink(String link) {
print('Received deep link: $link');
// Parse the URL
Uri uri = Uri.parse(link);
// Navigate based on the URL
if (uri.host == 'product' && uri.pathSegments.isNotEmpty) {
String productId = uri.pathSegments[0];
// Navigate to product screen
Navigator.pushNamed(context, '/product', arguments: productId);
}
}
@override
void dispose() {
_linkSubscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Deep Link Demo',
home: HomeScreen(),
routes: {
'/product': (context) => ProductScreen(),
},
);
}
}
🔹 Parsing URL Parameters
Extract data from deep link URLs:
void _handleDeepLink(String link) {
Uri uri = Uri.parse(link);
// Example: myapp://product/123?color=red&size=large
// Get path segments
if (uri.pathSegments.isNotEmpty) {
String productId = uri.pathSegments[0]; // "123"
// Get query parameters
String? color = uri.queryParameters['color']; // "red"
String? size = uri.queryParameters['size']; // "large"
// Navigate with all parameters
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProductScreen(
productId: productId,
color: color,
size: size,
),
),
);
}
}
class ProductScreen extends StatelessWidget {
final String productId;
final String? color;
final String? size;
ProductScreen({
required this.productId,
this.color,
this.size,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Product Details')),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Product ID: $productId', style: TextStyle(fontSize: 20)),
SizedBox(height: 10),
if (color != null)
Text('Color: $color', style: TextStyle(fontSize: 18)),
if (size != null)
Text('Size: $size', style: TextStyle(fontSize: 18)),
],
),
),
);
}
}
🔹 Universal Links (HTTPS)
Use regular web URLs to open your app:
Android Configuration (AndroidManifest.xml):
<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" />
<!-- HTTPS URLs -->
<data
android:scheme="https"
android:host="www.myapp.com"
android:pathPrefix="/product" />
</intent-filter>
iOS Configuration (Associated Domains):
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:www.myapp.com</string>
</array>
Universal Link Example:
URL:
https://www.myapp.com/product/123
- If app is installed: Opens in app
- If app is not installed: Opens in browser
🔹 Dynamic Route Generation
Use onGenerateRoute for flexible deep link handling:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Deep Link App',
onGenerateRoute: (settings) {
// Parse the route name
Uri uri = Uri.parse(settings.name ?? '/');
// Handle different routes
if (uri.pathSegments.isEmpty) {
return MaterialPageRoute(builder: (context) => HomeScreen());
}
// Handle /product/:id
if (uri.pathSegments[0] == 'product') {
if (uri.pathSegments.length > 1) {
String productId = uri.pathSegments[1];
return MaterialPageRoute(
builder: (context) => ProductScreen(productId: productId),
);
}
}
// Handle /user/:id
if (uri.pathSegments[0] == 'user') {
if (uri.pathSegments.length > 1) {
String userId = uri.pathSegments[1];
return MaterialPageRoute(
builder: (context) => UserScreen(userId: userId),
);
}
}
// Handle /category/:name
if (uri.pathSegments[0] == 'category') {
if (uri.pathSegments.length > 1) {
String category = uri.pathSegments[1];
return MaterialPageRoute(
builder: (context) => CategoryScreen(category: category),
);
}
}
// Default: 404 page
return MaterialPageRoute(builder: (context) => NotFoundScreen());
},
);
}
}
🔹 Testing Deep Links
Test your deep links during development:
🔸 Android (ADB Command):
# Test custom scheme
adb shell am start -W -a android.intent.action.VIEW \
-d "myapp://product/123" com.example.myapp
# Test HTTPS link
adb shell am start -W -a android.intent.action.VIEW \
-d "https://www.myapp.com/product/123" com.example.myapp
🔸 iOS (Terminal Command):
# Test custom scheme
xcrun simctl openurl booted "myapp://product/123"
# Test HTTPS link
xcrun simctl openurl booted "https://www.myapp.com/product/123"
🔸 Create Test HTML Page:
<!DOCTYPE html>
<html>
<head>
<title>Deep Link Test</title>
</head>
<body>
<h1>Test Deep Links</h1>
<a href="myapp://product/123">Open Product 123</a><br>
<a href="myapp://user/456">Open User 456</a><br>
<a href="https://www.myapp.com/product/789">Universal Link</a>
</body>
</html>
🔹 Complete Example
A full working deep linking implementation:
import 'package:flutter/material.dart';
import 'package:uni_links/uni_links.dart';
import 'dart:async';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State {
final GlobalKey navigatorKey = GlobalKey();
StreamSubscription? _linkSubscription;
@override
void initState() {
super.initState();
_initDeepLinks();
}
Future _initDeepLinks() async {
try {
final initialLink = await getInitialLink();
if (initialLink != null) {
_handleDeepLink(initialLink);
}
} catch (e) {
print('Error: $e');
}
_linkSubscription = linkStream.listen((String? link) {
if (link != null) {
_handleDeepLink(link);
}
});
}
void _handleDeepLink(String link) {
Uri uri = Uri.parse(link);
if (uri.host == 'product' && uri.pathSegments.isNotEmpty) {
String productId = uri.pathSegments[0];
navigatorKey.currentState?.pushNamed('/product/$productId');
}
}
@override
void dispose() {
_linkSubscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: navigatorKey,
title: 'Deep Link Demo',
home: HomeScreen(),
onGenerateRoute: (settings) {
Uri uri = Uri.parse(settings.name ?? '/');
if (uri.pathSegments.isEmpty) {
return MaterialPageRoute(builder: (context) => HomeScreen());
}
if (uri.pathSegments[0] == 'product' && uri.pathSegments.length > 1) {
return MaterialPageRoute(
builder: (context) => ProductScreen(
productId: uri.pathSegments[1],
),
);
}
return MaterialPageRoute(builder: (context) => HomeScreen());
},
);
}
}
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Home')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Deep Link Demo App', style: TextStyle(fontSize: 24)),
SizedBox(height: 20),
Text('Try opening: myapp://product/123'),
],
),
),
);
}
}
class ProductScreen extends StatelessWidget {
final String productId;
ProductScreen({required this.productId});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Product Details')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.shopping_bag, size: 100, color: Colors.blue),
SizedBox(height: 20),
Text(
'Product ID: $productId',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
SizedBox(height: 20),
Text('Opened via Deep Link!'),
],
),
),
);
}
}