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!'),
          ],
        ),
      ),
    );
  }
}

🧠 Test Your Knowledge

What is the purpose of deep linking?