Implementing Custom Deep Link Handling Logic in Flutter

Deep links are a powerful mechanism for directing users to specific sections within a mobile application. In Flutter, effectively handling custom deep links requires a strategic approach to ensure a seamless user experience. This blog post will guide you through implementing custom deep link handling logic in a Flutter application, covering setup, best practices, and advanced techniques.

What are Deep Links?

Deep links are Uniform Resource Identifiers (URIs) that direct users to specific content within a mobile app rather than just opening the app’s home page. They enhance user experience by directly taking users to relevant content, whether it’s a specific product, a promotion, or a particular feature.

Why Implement Custom Deep Link Handling?

  • Improved User Experience: Direct users straight to relevant content, making navigation smoother and more intuitive.
  • Enhanced Marketing Campaigns: Track and optimize user engagement through custom deep links in promotional materials.
  • Seamless Integration: Facilitate seamless integration with web content, other apps, and promotional campaigns.

Setting Up Deep Link Handling in Flutter

To implement deep link handling, you’ll need to configure both the Android and iOS platforms and then use Flutter code to handle incoming links.

Step 1: Android Configuration

Update your AndroidManifest.xml file (located at android/app/src/main/AndroidManifest.xml) to register an intent filter for your desired URI scheme.

<activity
    android:name=".MainActivity"
    android:launchMode="singleTask">
    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
    </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="yourcustomscheme" android:host="yourapp.com" />
    </intent-filter>
</activity>
  • Ensure android:launchMode="singleTask" to handle the deep link properly when the app is already running.
  • Replace yourcustomscheme with your desired scheme (e.g., myapp, customapp).
  • Set the android:host to your domain (e.g., yourapp.com).

Step 2: iOS Configuration

For iOS, you’ll need to configure the Info.plist file (located at ios/Runner/Info.plist).

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>yourcustomscheme</string>
        </array>
        <key>CFBundleURLName</key>
        <string>com.example.yourapp</string>
    </dict>
</array>
  • Replace yourcustomscheme with your custom scheme.
  • Set CFBundleURLName to your app’s bundle identifier.

Additionally, for Universal Links (using https scheme), you need to configure Associated Domains in your Xcode project and upload an apple-app-site-association file to your website. That file should be placed at `/.well-known/apple-app-site-association` and `/apple-app-site-association` to support older versions of iOS.

Step 3: Flutter Code for Handling Deep Links

Use the uni_links package to listen for incoming deep links in your Flutter application.

dependencies:
  flutter:
    sdk: flutter
  uni_links: ^0.5.1

Then, implement the following Flutter code to listen for deep links:

import 'package:flutter/material.dart';
import 'package:uni_links/uni_links.dart';
import 'package:flutter/services.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State {
  String? _latestLink = 'Unknown';
  StreamSubscription? _sub;

  @override
  void initState() {
    super.initState();
    _initDeepLinkHandling();
  }

  @override
  void dispose() {
    _sub?.cancel();
    super.dispose();
  }

  Future<void> _initDeepLinkHandling() async {
    // Platform messages may fail, so we use a try/catch PlatformException.
    try {
      final initialLink = await getInitialLink();
      if (initialLink != null) {
        print('Initial Link: $initialLink');
        _updateLink(initialLink);
      }
    } on PlatformException {
      print('Failed to get initial link.');
    }

    _sub = linkStream.listen((String? link) {
      if (link != null) {
        print('Incoming Link: $link');
        _updateLink(link);
      }
    }, onError: (err) {
      print('Error: $err');
    });
  }

  void _updateLink(String link) {
    setState(() {
      _latestLink = link;
      _handleDeepLink(link);
    });
  }

  void _handleDeepLink(String link) {
    // Parse the link and navigate to the relevant section of the app.
    final uri = Uri.parse(link);
    if (uri.scheme == 'yourcustomscheme' && uri.host == 'yourapp.com') {
      final path = uri.pathSegments;
      if (path.isNotEmpty) {
        switch (path[0]) {
          case 'product':
            // Navigate to product details page.
            print('Navigating to product: ${path[1]}');
            Navigator.of(context).push(MaterialPageRoute(
              builder: (context) => ProductDetailsPage(productId: path[1]),
            ));
            break;
          case 'profile':
            // Navigate to user profile page.
            print('Navigating to profile: ${path[1]}');
            Navigator.of(context).push(MaterialPageRoute(
              builder: (context) => ProfilePage(userId: path[1]),
            ));
            break;
          default:
            print('Unknown path: ${path[0]}');
        }
      }
    } else {
      print('Invalid deep link: $link');
    }
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Deep Link Example'),
        ),
        body: Center(
          child: Text('Latest Link: $_latestLinkn'),
        ),
      ),
    );
  }
}

class ProductDetailsPage extends StatelessWidget {
  final String productId;
  ProductDetailsPage({required this.productId});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Product Details'),
      ),
      body: Center(
        child: Text('Product ID: $productId'),
      ),
    );
  }
}

class ProfilePage extends StatelessWidget {
  final String userId;
  ProfilePage({required this.userId});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Profile Page'),
      ),
      body: Center(
        child: Text('User ID: $userId'),
      ),
    );
  }
}
  • The _initDeepLinkHandling() method is responsible for initializing deep link listening. It gets the initial link (if the app was opened via a deep link) and then listens for subsequent deep links.
  • The linkStream provides a stream of incoming deep links, allowing you to react in real-time.
  • The _handleDeepLink() method parses the link and navigates to the relevant section of the app.

Advanced Techniques for Deep Link Handling

1. Using Universal Links

Universal Links are standard HTTP or HTTPS links that redirect users to specific content within your app, improving security and SEO benefits over custom schemes. Implementing Universal Links requires uploading an `apple-app-site-association` file to your domain and configuring associated domains in your iOS project.

2. Deferred Deep Linking

Deferred deep linking allows you to track and attribute installs resulting from deep links, especially when a user installs the app after clicking a deep link but before opening it. Use services like Branch or Adjust to handle deferred deep linking, providing a seamless experience even if the app wasn’t initially installed.

3. Error Handling and Validation

Always include error handling to manage unexpected deep link formats or incorrect parameters. Validate parameters to ensure they conform to the expected data types and constraints.

Best Practices for Implementing Custom Deep Link Handling

  • Use Consistent URI Schemes: Choose a consistent and descriptive URI scheme.
  • Handle Edge Cases: Handle cases where the app isn’t installed or when parameters are missing or invalid.
  • Test Thoroughly: Test deep links on both Android and iOS devices to ensure they function as expected.
  • Document Your Deep Links: Maintain clear documentation of your deep link structure and parameters.
  • Ensure Security: Sanitize and validate parameters passed through deep links to prevent security vulnerabilities.

Conclusion

Implementing custom deep link handling in Flutter involves configuring native platforms, listening for deep links using the uni_links package, and navigating users to specific sections of your app. By following the steps and best practices outlined in this guide, you can create a seamless and engaging user experience through effective deep linking.