Flutter Deep Links Guide
Complete step-by-step guide to implement, configure, and test deep links in your Flutter application
Table of Contents
1. Overview & Prerequisites
What are Flutter Deep Links?
Flutter deep links allow users to navigate directly to specific content within your app from external sources like websites, emails, or other apps. Flutter provides excellent cross-platform deep linking support through GoRouter and platform-specific configurations.
Types of Deep Links
Custom URL Schemes
App-specific URL schemes
myapp://product/123
Universal Links (iOS)
Verified HTTPS URLs for iOS
https://domain.com/product/123
App Links (Android)
Verified HTTPS URLs for Android
https://domain.com/product/123
- • Cross-platform deep linking with single codebase
- • Seamless integration with GoRouter
- • Automatic fallback to web when app isn't installed
- • Better user engagement and retention
- • Unified URL handling across platforms
Prerequisites
- • Flutter 3.0+
- • Dart 2.17+
- • GoRouter package
- • Android Studio / Xcode
- • Managed domain and hosting for verification files
- • Automatic HTTPS with valid SSL certificates
- • Auto-generated assetlinks.json and AASA files
- • Seamless cross-platform verification support
2. GoRouter Setup
Install GoRouter
GoRouter is the recommended navigation package for Flutter that provides excellent deep linking support.
dependencies: flutter: sdk: flutter go_router: ^12.0.0 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^3.0.0
Basic GoRouter Configuration
import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { MyApp({super.key}); final GoRouter _router = GoRouter( initialLocation: '/', routes: [ GoRoute( path: '/', builder: (context, state) => const HomeScreen(), ), GoRoute( path: '/product/:productId', builder: (context, state) { final productId = state.pathParameters['productId']!; return ProductScreen(productId: productId); }, ), GoRoute( path: '/profile/:userId', builder: (context, state) { final userId = state.pathParameters['userId']!; return ProfileScreen(userId: userId); }, ), GoRoute( path: '/dashboard', builder: (context, state) => const DashboardScreen(), ), ], ); @override Widget build(BuildContext context) { return MaterialApp.router( routerConfig: _router, title: 'Flutter Deep Links Demo', theme: ThemeData( primarySwatch: Colors.blue, ), ); } }
Advanced GoRouter Configuration
final GoRouter _router = GoRouter( initialLocation: '/', routes: [ ShellRoute( builder: (context, state, child) => ScaffoldWithNavigation(child: child), routes: [ GoRoute( path: '/', builder: (context, state) => const HomeScreen(), routes: [ GoRoute( path: 'product/:productId', builder: (context, state) { final productId = state.pathParameters['productId']!; return ProductScreen(productId: productId); }, ), GoRoute( path: 'profile/:userId', builder: (context, state) { final userId = state.pathParameters['userId']!; return ProfileScreen(userId: userId); }, ), ], ), GoRoute( path: '/dashboard', builder: (context, state) => const DashboardScreen(), ), ], ), ], errorBuilder: (context, state) => ErrorScreen(error: state.error), );
3. Android Configuration
Add Intent Filters
Configure your Android app to handle deep links by adding intent filters to your main activity.
<activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:windowSoftInputMode="adjustResize"> <!-- Standard activity intent filter --> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <!-- Deep link 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" /> </intent-filter> <!-- Custom URL scheme 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" /> <data android:scheme="myapp" /> </intent-filter> </activity>
Digital Asset Links
For Android App Links, you need to verify domain ownership. Our service automatically generates and hosts the required assetlinks.json file.
- • The assetlinks.json file is automatically generated and hosted by our service
- • File is accessible at:
https://yourdomain.com/.well-known/assetlinks.json
- • Must include your app's SHA256 certificate fingerprint
- • Android will verify this file when users install your app
4. iOS Configuration
Add Associated Domains
Configure your iOS app to handle Universal Links by adding the Associated Domains capability.
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <!-- Other configurations --> <key>CFBundleURLTypes</key> <array> <dict> <key>CFBundleURLName</key> <string>com.yourcompany.yourapp</string> <key>CFBundleURLSchemes</key> <array> <string>myapp</string> </array> </dict> </array> <key>com.apple.developer.associated-domains</key> <array> <string>applinks:yourdomain.com</string> <string>applinks:www.yourdomain.com</string> </array> </dict> </plist>
Apple App Site Association
Our service automatically generates and hosts the apple-app-site-association file required for Universal Links.
- • AASA file is automatically generated and hosted
- • Accessible at:
https://yourdomain.com/.well-known/apple-app-site-association
- • Includes proper Content-Type headers
- • Supports both applinks and webcredentials
5. URL Handling
Screen Implementation
import 'package:flutter/material.dart'; class ProductScreen extends StatefulWidget { final String productId; const ProductScreen({super.key, required this.productId}); @override State<ProductScreen> createState() => _ProductScreenState(); } class _ProductScreenState extends State<ProductScreen> { @override void initState() { super.initState(); // Handle deep link parameters _handleDeepLink(); } void _handleDeepLink() { // Validate product ID if (widget.productId.isNotEmpty) { // Fetch product data or handle navigation print('Product ID from deep link: ${widget.productId}'); _loadProduct(widget.productId); } } void _loadProduct(String productId) { // Implement product loading logic // This could involve API calls, database queries, etc. } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Product Details'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'Product ID: ${widget.productId}', style: Theme.of(context).textTheme.headlineSmall, ), const SizedBox(height: 20), const Text('Product details will be loaded here'), ], ), ), ); } }
Advanced URL Handling
final GoRouter _router = GoRouter( initialLocation: '/', routes: [ GoRoute( path: '/', builder: (context, state) => const HomeScreen(), routes: [ GoRoute( path: 'product/:productId', builder: (context, state) { final productId = state.pathParameters['productId']!; final queryParams = state.queryParameters; // Handle query parameters final category = queryParams['category']; final source = queryParams['source']; return ProductScreen( productId: productId, category: category, source: source, ); }, ), GoRoute( path: 'search/:query', builder: (context, state) { final query = state.pathParameters['query']!; final decodedQuery = Uri.decodeComponent(query); return SearchScreen(query: decodedQuery); }, ), ], ), ], redirect: (context, state) { // Handle authentication redirects final isLoggedIn = AuthService.isLoggedIn(); final isLoginRoute = state.matchedLocation == '/login'; if (!isLoggedIn && !isLoginRoute) { return '/login'; } if (isLoggedIn && isLoginRoute) { return '/'; } return null; }, );
🔒 Security Considerations
Input Validation
Always validate and sanitize incoming URL parameters to prevent security vulnerabilities.
class ProductScreen extends StatefulWidget { final String productId; const ProductScreen({super.key, required this.productId}); @override State<ProductScreen> createState() => _ProductScreenState(); } class _ProductScreenState extends State<ProductScreen> { @override void initState() { super.initState(); _handleDeepLink(); } void _handleDeepLink() { // ✅ Good: Validate input first if (_isValidProductId(widget.productId)) { _loadProduct(widget.productId); } else { // Handle invalid product ID _showError('Invalid product ID'); } } bool _isValidProductId(String productId) { // Validate product ID format final regex = RegExp(r'^[a-zA-Z0-9_-]{1,50}$'); return regex.hasMatch(productId); } // ❌ Bad: Direct usage without validation // void _handleDeepLink() { // _loadProduct(widget.productId); // Could be dangerous // } }
Authentication & Authorization
- • Always verify user authentication before accessing sensitive content
- • Validate user permissions for accessed resources
- • Don't expose sensitive data through URL parameters
- • Use secure tokens for authenticated deep links
- • Implement rate limiting for URL processing
Best Security Practices
HTTPS Only
Always use HTTPS for deep links to prevent man-in-the-middle attacks and ensure data integrity.
Domain Verification
Both Android and iOS automatically verify domain ownership through verification files.
Graceful Fallbacks
Always handle invalid URLs gracefully without exposing sensitive information or crashing.
Path Restrictions
Use specific path patterns in verification files to limit which URLs open your app.
7. Testing & Debugging
Testing Methods
1. Android Testing (ADB)
adb shell am start \ -W -a android.intent.action.VIEW \ -d "https://yourdomain.com/product?id=123" \ com.yourcompany.yourapp
2. iOS Testing (Simulator)
xcrun simctl openurl booted "https://yourdomain.com/product?id=123"
3. Flutter Development
flutter run -d android flutter run -d ios flutter run --hot
Debugging Tools
- • Use Flutter DevTools for advanced debugging and navigation state inspection
- • Add print statements in your route builders for debugging
- • Test on physical devices for best results
- • Use Flutter Inspector to debug widget tree and navigation
- • Check platform-specific logs (Android Logcat, iOS Console)
8. Common Issues & Solutions
❌ Deep links not working
- • Check if the URL matches your GoRouter configuration
- • Verify domain verification files are accessible
- • Ensure proper Content-Type headers for verification files
- • Test on physical devices (not just simulators)
⚠️ Navigation not working
- • Verify GoRouter route configuration
- • Check parameter names match your route definitions
- • Ensure routes are properly registered
- • Test with simple URLs first
ℹ️ Platform-specific issues
- • Android: Check assetlinks.json accessibility and format
- • iOS: Verify Associated Domains capability and AASA file
- • Both: Ensure HTTPS is used for verification files
- • Both: Check for proper certificate fingerprints
✅ Best Practices
- • Always test on physical devices
- • Use consistent URL patterns across platforms
- • Implement proper error handling for invalid URLs
- • Keep GoRouter configuration simple and focused
- • Test deep links from various sources (Safari, Messages, etc.)