Deep Linking in Flutter: Complete Guide 2026

Learn how to implement deep links in Flutter apps with custom URL schemes, App Links, Universal Links, and deferred deep linking. Complete code examples included.


Deep Linking in Flutter: Complete Guide 2026

Deep linking in Flutter applications enables developers to create seamless user experiences by directing users to specific content within an app from external sources like emails, SMS messages, social media posts, or QR codes. Whether you're building an e-commerce app, a content platform, or a social networking application, implementing deep links in Flutter is essential for user engagement and retention.

This comprehensive guide walks you through everything you need to know about implementing deep links in Flutter, from basic configuration to advanced deferred deep linking scenarios.

Deep links are URLs that navigate users directly to specific screens or content within your Flutter mobile application, bypassing the home screen. There are three main types of deep links you can implement in Flutter:

  • Custom URL Schemes: Simple deep links using custom protocols (e.g., myapp://product/123)

  • Android App Links: Verified deep links that open your Android app automatically without disambiguation dialogs

  • iOS Universal Links: Verified deep links that open your iOS app seamlessly or fall back to the web

For a detailed understanding of how deep linking works across platforms, check out our complete technical guide on how deep linking works.

Deep linking provides numerous benefits for Flutter applications:

  • Improved User Experience: Direct users to relevant content instantly instead of forcing them to navigate through multiple screens

  • Higher Conversion Rates: Reduce friction in the user journey from marketing campaigns to in-app actions

  • Better Attribution: Track which marketing channels drive the most valuable in-app engagement

  • Enhanced Re-engagement: Bring dormant users back to specific features or content through targeted campaigns

  • Seamless Cross-Platform Experience: Maintain context when users switch between web and mobile

Learn more about the comprehensive benefits of mobile deep linking in our technical guide.

Prerequisites

Before implementing deep links in Flutter, ensure you have:

  • Flutter SDK installed (version 3.0 or higher recommended)

  • A Flutter project set up for both Android and iOS

  • Access to your domain's DNS settings (for App Links and Universal Links)

  • Basic understanding of Flutter navigation and routing

  • Developer accounts for Google Play Console and Apple Developer Program

The go_router package is the recommended approach for handling deep links in Flutter as it provides declarative routing with built-in deep link support.

Step 1: Add Dependencies

Add the following to your pubspec.yaml file:

dependencies: flutter: sdk: flutter go_router: ^13.0.0

Run flutter pub get to install the package.

Step 2: Configure Routes

Create a router configuration with deep link support:

import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; final GoRouter router = GoRouter( routes: [ GoRoute( path: '/', builder: (context, state) => const HomeScreen(), ), GoRoute( path: '/product/:id', builder: (context, state) { final productId = state.pathParameters['id']; return ProductDetailScreen(productId: productId ?? ''); }, ), GoRoute( path: '/category/:categoryName', builder: (context, state) { final categoryName = state.pathParameters['categoryName']; final sortBy = state.uri.queryParameters['sort']; return CategoryScreen( categoryName: categoryName ?? '', sortBy: sortBy, ); }, ), ], errorBuilder: (context, state) => const ErrorScreen(), );

Step 3: Update MaterialApp

Replace your MaterialApp with MaterialApp.router:

class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp.router( title: 'My Flutter App', routerConfig: router, ); } }

The uni_links package provides lower-level control over deep link handling.

Step 1: Add Dependency

dependencies: uni_links: ^0.5.1

Step 2: Handle Deep Links

import 'dart:async'; import 'package:uni_links/uni_links.dart'; import 'package:flutter/services.dart'; class DeepLinkHandler { StreamSubscription? _sub; Future initUniLinks(BuildContext context) async { // Handle deep link when app is already running _sub = uriLinkStream.listen((Uri? uri) { if (uri != null) { _handleDeepLink(context, uri); } }, onError: (err) { print('Deep link error: $err'); }); // Handle deep link when app is launched from closed state try { final initialUri = await getInitialUri(); if (initialUri != null) { _handleDeepLink(context, initialUri); } } on PlatformException { print('Failed to get initial URI'); } } void _handleDeepLink(BuildContext context, Uri uri) { print('Received deep link: $uri'); // Parse the URI and navigate accordingly if (uri.pathSegments.isNotEmpty) { final firstSegment = uri.pathSegments[0]; if (firstSegment == 'product' && uri.pathSegments.length > 1) { final productId = uri.pathSegments[1]; Navigator.pushNamed( context, '/product', arguments: {'id': productId}, ); } else if (firstSegment == 'category' && uri.pathSegments.length > 1) { final categoryName = uri.pathSegments[1]; Navigator.pushNamed( context, '/category', arguments: {'name': categoryName}, ); } } } void dispose() { _sub?.cancel(); } }

Configure Custom URL Schemes

Add the following to your android/app/src/main/AndroidManifest.xml inside the <activity> tag:

<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>

Configure Android App Links

For verified App Links, add this 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:pathPrefix="/app" /> </intent-filter>

Create assetlinks.json File

Host this file at https://yourdomain.com/.well-known/assetlinks.json:

[{ "relation": ["delegate_permission/common.handle_all_urls"], "target": { "namespace": "android_app", "package_name": "com.yourcompany.yourapp", "sha256_cert_fingerprints": [ "YOUR_SHA256_FINGERPRINT_HERE" ] } }]

Get your SHA256 fingerprint using:

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

For production builds, use your release keystore. Learn more about Android App Links configuration.

Configure Custom URL Schemes

Open ios/Runner/Info.plist and add:

<key>CFBundleURLTypes</key> <array> <dict> <key>CFBundleTypeRole</key> <string>Editor</string> <key>CFBundleURLName</key> <string>com.yourcompany.yourapp</string> <key>CFBundleURLSchemes</key> <array> <string>myapp</string> </array> </dict> </array>

Configure iOS Universal Links

In Xcode, select your project target, go to "Signing & Capabilities", and add "Associated Domains":

applinks:yourdomain.com

Create and host an apple-app-site-association file at https://yourdomain.com/.well-known/apple-app-site-association:

{ "applinks": { "apps": [], "details": [ { "appID": "TEAMID.com.yourcompany.yourapp", "paths": ["/app/*", "/product/*"] } ] } }

Replace TEAMID with your Apple Developer Team ID. For a complete guide on iOS Universal Links, see our iOS Universal Links guide.

Deferred deep links are special deep links that work even when your app isn't installed. They remember the intended destination and navigate users there after they install and open the app for the first time.

Understanding the difference between regular and deferred deep linking is crucial. Check out our technical comparison guide to learn more.

Using Smler for Deferred Deep Linking

Smler provides a simple API to create deferred deep links that work across platforms. Here's how to integrate it in your Flutter app:

Step 1: Create a Deferred Deep Link

Use the Smler API to create a deferred deep link:

import 'package:http/http.dart' as http; import 'dart:convert'; Future createDeferredDeepLink({ required String productId, required String campaign, }) async { final response = await http.post( Uri.parse('https://api.smler.io/v1/links'), headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json', }, body: jsonEncode({ 'url': 'https://yourdomain.com/product/$productId', 'iosAppUrl': 'myapp://product/$productId', 'androidAppUrl': 'myapp://product/$productId', 'iosStoreUrl': 'https://apps.apple.com/app/your-app', 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.yourcompany.yourapp', 'customDomain': 'yourdomain.com', 'tags': [campaign], }), ); if (response.statusCode == 200) { final data = jsonDecode(response.body); return data['shortUrl']; } else { throw Exception('Failed to create deep link'); } }

Step 2: Handle First App Launch

import 'package:shared_preferences/shared_preferences.dart'; class FirstLaunchHandler { static const String _firstLaunchKey = 'is_first_launch'; Future handleFirstLaunch(BuildContext context) async { final prefs = await SharedPreferences.getInstance(); final isFirstLaunch = prefs.getBool(_firstLaunchKey) ?? true; if (isFirstLaunch) { // Check for deferred deep link data await _checkDeferredDeepLink(context); await prefs.setBool(_firstLaunchKey, false); } } Future _checkDeferredDeepLink(BuildContext context) async { // Query Smler API for deferred deep link data // This is typically done by checking device fingerprint try { final response = await http.get( Uri.parse('https://api.smler.io/v1/install-data'), headers: {'Authorization': 'Bearer YOUR_API_KEY'}, ); if (response.statusCode == 200) { final data = jsonDecode(response.body); if (data['deepLink'] != null) { final uri = Uri.parse(data['deepLink']); _handleDeepLink(context, uri); } } } catch (e) { print('Error checking deferred deep link: $e'); } } void _handleDeepLink(BuildContext context, Uri uri) { // Navigate to the appropriate screen // Implementation depends on your routing strategy } }

For detailed implementation steps, refer to our guide on how to generate deferred deep links.

Testing on Android

Use ADB to test deep links on Android devices or emulators:

# Test custom URL scheme adb shell am start -W -a android.intent.action.VIEW -d "myapp://product/123" com.yourcompany.yourapp # Test App Link adb shell am start -W -a android.intent.action.VIEW -d "https://yourdomain.com/product/123" com.yourcompany.yourapp

For comprehensive testing strategies, see our complete guide to testing deep links on Android.

Testing on iOS

For iOS, you can test using the Terminal:

# Test custom URL scheme xcrun simctl openurl booted "myapp://product/123" # Note: Universal Links cannot be tested from Safari address bar # Create an HTML page with a link and open it in Safari

Check our iOS deep link testing guide for detailed testing procedures.

Testing with Smler

Smler provides a testing environment where you can validate your deep link configuration:

  1. Create a test deep link in your Smler dashboard

  2. Use the link preview tool to see how the link behaves on different devices

  3. Check the analytics to verify click tracking and device detection

Handling Query Parameters

GoRoute( path: '/search', builder: (context, state) { final query = state.uri.queryParameters['q']; final filter = state.uri.queryParameters['filter']; final sort = state.uri.queryParameters['sort']; return SearchScreen( query: query ?? '', filter: filter, sortBy: sort, ); }, ),

Nested Route Navigation

GoRoute( path: '/profile/:userId', builder: (context, state) { final userId = state.pathParameters['userId']; return ProfileScreen(userId: userId ?? ''); }, routes: [ GoRoute( path: 'posts', builder: (context, state) { final userId = state.pathParameters['userId']; return UserPostsScreen(userId: userId ?? ''); }, ), GoRoute( path: 'followers', builder: (context, state) { final userId = state.pathParameters['userId']; return FollowersScreen(userId: userId ?? ''); }, ), ], ),

Authentication Guards

final GoRouter router = GoRouter( routes: [...], redirect: (context, state) { final isAuthenticated = // Check authentication state final isAuthRoute = state.matchedLocation == '/login'; if (!isAuthenticated && !isAuthRoute) { // Save the deep link destination return '/login?redirect=${Uri.encodeComponent(state.matchedLocation)}'; } if (isAuthenticated && isAuthRoute) { return '/'; } return null; // No redirect needed }, );

Understanding how users interact with your deep links is essential for optimization. Smler provides comprehensive link-level analytics that track:

  • Click-through rates by device, location, and platform

  • Installation attribution for deferred deep links

  • Conversion tracking from link click to in-app action

  • User journey visualization

Implementing Analytics Tracking

void _handleDeepLink(BuildContext context, Uri uri) async { // Track deep link event await analytics.logEvent( name: 'deep_link_opened', parameters: { 'link_url': uri.toString(), 'path': uri.path, 'source': uri.queryParameters['utm_source'] ?? 'unknown', 'campaign': uri.queryParameters['utm_campaign'] ?? 'unknown', }, ); // Navigate to destination // ... }

Issue 1: Deep Links Not Working on iOS

Solution:

  • Verify your apple-app-site-association file is accessible via HTTPS

  • Ensure the file has no .json extension

  • Check that Associated Domains are correctly configured in Xcode

  • Universal Links don't work when typed in Safari's address bar; they must be clicked from another app

Issue 2: App Links Not Verified on Android

Solution:

  • Verify assetlinks.json is accessible at /.well-known/assetlinks.json

  • Ensure SHA256 fingerprints match your app's signing certificate

  • Set android:autoVerify="true" in the intent filter

  • Test using adb shell pm get-app-links com.yourcompany.yourapp

Issue 3: Deep Link Not Triggering When App is Closed

Solution:

  • Ensure you're checking getInitialUri() in your initialization code

  • Call the deep link handler in your app's main widget initState() method

  • Use WidgetsBinding.instance.addPostFrameCallback() for navigation after the first frame

Issue 4: Parameters Not Being Parsed Correctly

Solution:

  • Use Uri.parse() to properly parse the deep link URL

  • Access path parameters via uri.pathSegments

  • Access query parameters via uri.queryParameters

  • Always validate and sanitize parameters before use

Best Practices for Flutter Deep Linking

1. Use Branded Short Links

Instead of long, unwieldy URLs, use branded short links that are easier to share and track. Smler allows you to create custom branded domains for your deep links:

// Instead of: https://yourdomain.com/products/electronics/smartphones/iphone-15-pro-max?color=blue&storage=256gb // Use: https://shop.yourdomain.com/abc123

2. Implement Proper Error Handling

void _handleDeepLink(BuildContext context, Uri uri) { try { // Validate URI structure if (uri.pathSegments.isEmpty) { _navigateToHome(context); return; } // Parse and navigate final route = _parseRoute(uri); if (route != null) { Navigator.pushNamed(context, route); } else { _showErrorDialog(context, 'Invalid link'); } } catch (e) { _showErrorDialog(context, 'Error opening link'); _logError(e); } }

3. Provide Fallback URLs

Always configure fallback URLs for scenarios where the app isn't installed or can't be opened:

{ "url": "https://yourdomain.com/product/123", "iosAppUrl": "myapp://product/123", "androidAppUrl": "myapp://product/123", "iosFallbackUrl": "https://yourdomain.com/product/123", "androidFallbackUrl": "https://yourdomain.com/product/123" }

4. Use Device-Based Routing

Automatically redirect users to the App Store or Play Store based on their device. Smler's device-based routing handles this automatically.

5. Test Across Multiple Scenarios

  • App installed, app running in foreground

  • App installed, app running in background

  • App installed, app completely closed

  • App not installed (deferred deep linking)

  • Different Android manufacturers (Samsung, Xiaomi, OnePlus, etc.)

  • Different iOS versions

6. Secure Your Deep Links

void _handleDeepLink(BuildContext context, Uri uri) { // Validate the domain if (uri.host != 'yourdomain.com' && uri.host != 'shop.yourdomain.com') { print('Untrusted deep link domain: ${uri.host}'); return; } // Sanitize parameters final productId = uri.pathSegments.length > 1 ? _sanitizeId(uri.pathSegments[1]) : null; if (productId == null) { return; } // Proceed with navigation // ... } String? _sanitizeId(String id) { // Only allow alphanumeric characters and hyphens final regex = RegExp(r'^[a-zA-Z0-9-]+$'); return regex.hasMatch(id) ? id : null; }

E-Commerce Product Sharing

Enable users to share products directly from your Flutter e-commerce app:

Future shareProduct(Product product) async { final deepLink = await createDeferredDeepLink( productId: product.id, campaign: 'product_share', ); await Share.share( 'Check out ${product.name} on our app!\n$deepLink', subject: product.name, ); }

Social Media Content Sharing

GoRoute( path: '/post/:postId', builder: (context, state) { final postId = state.pathParameters['postId']; final commentId = state.uri.queryParameters['comment']; return PostDetailScreen( postId: postId ?? '', highlightCommentId: commentId, ); }, ),

Email Campaign Integration

Track email campaign performance with UTM parameters:

void _handleDeepLink(BuildContext context, Uri uri) { final utmSource = uri.queryParameters['utm_source']; final utmMedium = uri.queryParameters['utm_medium']; final utmCampaign = uri.queryParameters['utm_campaign']; // Track campaign attribution analytics.logEvent( name: 'campaign_link_opened', parameters: { 'source': utmSource ?? 'unknown', 'medium': utmMedium ?? 'unknown', 'campaign': utmCampaign ?? 'unknown', }, ); // Continue with navigation // ... }

You can easily add UTM parameters using Smler's UTM builder tool.

QR Code Campaigns

Create deep links for offline marketing materials like posters, product packaging, or event flyers:

Future createQRDeepLink(String campaignId) async { final deepLink = await createDeferredDeepLink( productId: campaignId, campaign: 'qr_code_scan', ); // Generate QR code from deep link return deepLink; }

Learn more about QR code deep linking for offline marketing. You can also generate QR codes directly using Smler's QR code generator.

If you're currently using Firebase Dynamic Links in your Flutter app, you should migrate before the service is fully discontinued. Smler provides a drop-in replacement with enhanced features.

Migration Steps

  1. Create a Smler account at app.smler.io

  2. Set up your custom domain in the Smler dashboard

  3. Update your deep link creation code to use Smler's API instead of Firebase

  4. Configure verification files for App Links and Universal Links

  5. Test thoroughly before deprecating Firebase Dynamic Links

For detailed migration instructions, see our Firebase Dynamic Links to Smler migration guide.

Why Choose Smler Over Firebase?

  • Continued Support: Unlike Firebase Dynamic Links, Smler is actively maintained and improved

  • Better Analytics: More detailed link-level analytics with geographic and device breakdowns

  • Custom Domains: Full support for branded short links with your domain

  • Compliance Features: Built-in support for regulatory requirements like TRAI compliance

  • Competitive Pricing: Generous free tier for startups and indie developers

Compare Smler with other alternatives in our Firebase Dynamic Links alternatives tier list.

Lazy Loading Routes

For large Flutter apps, use lazy loading to improve initial load time:

GoRoute( path: '/heavy-feature/:id', builder: (context, state) { return FutureBuilder( future: loadHeavyFeature(), builder: (context, snapshot) { if (snapshot.hasData) { return HeavyFeatureScreen( data: snapshot.data, id: state.pathParameters['id'], ); } return const LoadingScreen(); }, ); }, ),

Caching Deep Link Data

class DeepLinkCache { static final Map _cache = {}; static Future fetchData(String key, Future Function() fetcher) async { if (_cache.containsKey(key)) { return _cache[key]; } final data = await fetcher(); _cache[key] = data; return data; } static void clear() { _cache.clear(); } }

Preloading Critical Resources

void _handleDeepLink(BuildContext context, Uri uri) async { if (uri.pathSegments.isNotEmpty && uri.pathSegments[0] == 'product') { final productId = uri.pathSegments.length > 1 ? uri.pathSegments[1] : null; if (productId != null) { // Preload product data final productData = productService.preloadProduct(productId); // Navigate once data is ready Navigator.push( context, MaterialPageRoute( builder: (context) => FutureBuilder( future: productData, builder: (context, snapshot) { if (snapshot.hasData) { return ProductDetailScreen(product: snapshot.data); } return const LoadingScreen(); }, ), ), ); } } }

Enable Debug Logging

class DeepLinkLogger { static void log(String message, {Object? data}) { if (kDebugMode) { print('[DeepLink] $message'); if (data != null) { print('[DeepLink Data] $data'); } } } static void logError(String message, Object error, StackTrace stackTrace) { print('[DeepLink Error] $message'); print('[Error] $error'); print('[StackTrace] $stackTrace'); // Send to crash reporting service // FirebaseCrashlytics.instance.recordError(error, stackTrace); } }

Real-Time Deep Link Monitoring

Use Smler's webhook notifications to monitor deep link activity in real-time:

// Configure webhook endpoint in Smler dashboard // Receive notifications for: // - Link clicks // - App installs // - Conversion events

Conclusion

Implementing deep links in Flutter requires careful configuration across both Android and iOS platforms, but the benefits are substantial. By following this guide, you can create seamless user experiences that drive engagement, improve attribution, and increase conversions.

Key takeaways:

  • Use go_router for modern, declarative deep link routing in Flutter

  • Configure both custom URL schemes and platform-specific App Links/Universal Links

  • Implement deferred deep linking for install attribution with services like Smler

  • Test thoroughly across different devices, platforms, and app states

  • Monitor performance and user behavior with comprehensive analytics

  • Secure your deep links by validating domains and sanitizing parameters

Whether you're building an e-commerce app, a social platform, or a content application, deep linking is essential for modern mobile experiences. Start implementing deep links in your Flutter app today with Smler's platform.

For more resources and technical guides, explore our documentation or browse additional deep linking use cases. If you need help with implementation, our support team is here to assist you.

Published with LeafPad