Flutter Deep Links Guide

Complete step-by-step guide to implement, configure, and test deep links in your Flutter application

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
Key Benefits
  • • 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

Development
  • • Flutter 3.0+
  • • Dart 2.17+
  • • GoRouter package
  • • Android Studio / Xcode
What We Provide
  • • 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.

pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  go_router: ^12.0.0
  
dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^3.0.0

Basic GoRouter Configuration

lib/main.dart
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

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

android/app/src/main/AndroidManifest.xml
<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.

Important
  • • 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.

ios/Runner/Info.plist
<?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.

Automated Setup
  • • 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

lib/screens/product_screen.dart
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

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

Critical

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

Important
  • • 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.)

🎉 You're All Set!

Your Flutter app should now handle deep links seamlessly across both platforms. Test thoroughly on physical devices.