Implementing Deep Linking in Flutter Applications

Deep linking is a crucial feature in modern mobile applications, allowing users to navigate directly to specific content within an app from external sources such as websites, emails, or social media links. In Flutter, implementing deep linking can significantly enhance user experience and engagement. This article provides a comprehensive guide to understanding and implementing deep linking in Flutter applications.

What is Deep Linking?

Deep linking is a technique that directs users to a specific location within an application rather than just opening the app’s homepage. This can be used for various purposes, such as:

  • Navigating users directly to a product page from an advertisement.
  • Directing users to a specific article from a social media post.
  • Handling password reset links.
  • Implementing referral programs.

Why Use Deep Linking in Flutter?

  • Improved User Experience: Reduces friction by taking users directly to relevant content.
  • Increased Engagement: Enhances user interaction and conversion rates.
  • Seamless Navigation: Facilitates navigation from external sources into specific app sections.

Types of Deep Linking

There are primarily two types of deep links:

  • Custom Scheme Deep Links: Uses a custom URL scheme (e.g., myapp://path/to/content).
  • Universal Links (Android App Links/iOS Universal Links): Uses standard HTTP/HTTPS URLs that are associated with your website.

Custom Scheme Deep Links

Custom scheme deep links are easier to implement but less secure. They rely on a custom URL scheme registered by the app. If multiple apps register the same scheme, the system presents a disambiguation dialog to the user.

Universal Links

Universal Links provide a more secure and reliable deep linking solution. They use standard web URLs (http:// or https://) and require verification through a file hosted on your website.

Implementing Deep Linking in Flutter: Custom Scheme

Let’s start by implementing custom scheme deep links in a Flutter application.

Step 1: Add Dependency

Add the uni_links package to your pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
  uni_links: ^0.5.1

Run flutter pub get to install the dependency.

Step 2: Configure Native Platforms

Android Configuration

Open android/app/src/main/AndroidManifest.xml and add the following intent filter inside your main activity (<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" android:host="open" />
</intent-filter>

Replace myapp with your desired custom scheme. In this case, a URL like `myapp://open?param1=value1&param2=value2` would trigger this intent filter.

iOS Configuration

Open ios/Runner/Info.plist and add the following inside the <dict> tag:

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>myapp</string>
        </array>
        <key>CFBundleURLName</key>
        <string>com.example.myapp</string>
    </dict>
</array>

Replace myapp with your desired custom scheme, and com.example.myapp with your app’s bundle identifier.

Step 3: Implement Deep Link Handling in Flutter

In your Flutter application, listen for incoming deep links using the uni_links package. Create a Flutter service or use your main app widget to handle incoming links.

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

class DeepLinkingService {
  StreamSubscription? _sub;

  void initialize(BuildContext context) {
    _initDeepLinkHandling(context);
  }

  Future<void> _initDeepLinkHandling(BuildContext context) async {
    // Platform messages may fail, so we use a try/catch PlatformException.
    try {
      final initialLink = await getInitialLink();
      if (initialLink != null) {
        _handleDeepLink(context, initialLink);
      }
    } catch (e) {
      print('Error getting initial link: $e');
    }

    // Attach a listener to the stream
    _sub = linkStream.listen((String? link) {
      if (link != null) {
        _handleDeepLink(context, link);
      }
    }, onError: (err) {
      print('Error during deep link stream: $err');
    });
  }

  void _handleDeepLink(BuildContext context, String link) {
    // Parse the link and navigate accordingly
    Uri uri = Uri.parse(link);

    // Example: myapp://open?route=product&id=123
    if (uri.scheme == 'myapp' && uri.host == 'open') {
      String? route = uri.queryParameters['route'];
      String? id = uri.queryParameters['id'];

      if (route == 'product' && id != null) {
        // Navigate to product details page
        Navigator.push(
          context,
          MaterialPageRoute(
            builder: (context) => ProductDetailsPage(productId: id),
          ),
        );
      } else {
        // Handle other routes or display an error
        print('Unknown route: $route');
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Unknown deep link route')),
        );
      }
    } else {
      print('Invalid deep link: $link');
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Invalid deep link')),
      );
    }
  }

  void dispose() {
    _sub?.cancel(); // Important to cancel the subscription
  }
}

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

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

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

class _MyAppState extends State<MyApp> {
  final _deepLinkingService = DeepLinkingService();

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _deepLinkingService.initialize(context);
    });
  }

  @override
  void dispose() {
    _deepLinkingService.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Deep Linking Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: Text('Home'),
        ),
        body: Center(
          child: Text('Welcome to the app!'),
        ),
      ),
    );
  }
}

Key points in this code:

  • `DeepLinkingService` Class: A class responsible for initializing deep link handling, listening for incoming links, and disposing of resources.
  • `_initDeepLinkHandling()` Method: Initializes deep linking by retrieving the initial link (if the app was opened via a deep link) and listening for subsequent links.
  • `_handleDeepLink()` Method: Parses the deep link, extracts relevant information (e.g., route and ID), and navigates the user to the appropriate screen.
  • `ProductDetailsPage` Widget: A placeholder widget representing a product details screen.

Implementing Deep Linking in Flutter: Universal Links

Implementing Universal Links involves configuring both your Flutter app and your website. This process verifies that you own both the app and the website, providing a more secure linking experience.

Step 1: Configure the Website

Create an apple-app-site-association file and host it on your website under the .well-known directory. This file associates your website with your app.

Example apple-app-site-association file:
{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "YOUR_TEAM_ID.com.example.myapp",
        "paths": [
          "/product/*",
          "/article/*"
        ]
      }
    ]
  }
}

Replace YOUR_TEAM_ID.com.example.myapp with your app’s Team ID and Bundle Identifier. The paths array specifies which URL paths should be handled by your app.
Ensure the file is served with the MIME type `application/json` and is accessible over HTTPS.

Android equivalent assetlinks.json file (place in /.well-known directory):

[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.example.myapp",
    "sha256_cert_fingerprints":
    ["YOUR_SHA256_FINGERPRINT"]
  }
}]

Replace `com.example.myapp` with your app’s package name and `YOUR_SHA256_FINGERPRINT` with the SHA256 fingerprint of your signing certificate. The SHA256 fingerprint can be obtained using keytool.

Step 2: Configure the Flutter App

iOS Configuration

Open your Xcode project, navigate to your target’s settings, and enable the “Associated Domains” capability. Add an entry for your website’s domain with the applinks: prefix:

applinks:yourdomain.com

Replace yourdomain.com with your actual domain.

Android Configuration

Add the following to your `AndroidManifest.xml` file, within the `` tag:


<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="http" android:host="yourdomain.com" />
    <data android:scheme="https" android:host="yourdomain.com" />
</intent-filter>

Replace `yourdomain.com` with your actual domain.

Make sure `android:autoVerify=”true”` attribute is present to enable automatic verification of your App Links.

Step 3: Handle the Universal Links in Flutter

Use the same uni_links package to listen for and handle Universal Links in your Flutter app.


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

class DeepLinkingService {
  StreamSubscription? _sub;

  void initialize(BuildContext context) {
    _initDeepLinkHandling(context);
  }

  Future<void> _initDeepLinkHandling(BuildContext context) async {
    try {
      final initialLink = await getInitialLink();
      if (initialLink != null) {
        _handleDeepLink(context, initialLink);
      }
    } catch (e) {
      print('Error getting initial link: $e');
    }

    _sub = linkStream.listen((String? link) {
      if (link != null) {
        _handleDeepLink(context, link);
      }
    }, onError: (err) {
      print('Error during deep link stream: $err');
    });
  }


  void _handleDeepLink(BuildContext context, String link) {
    Uri uri = Uri.parse(link);

    // Example: https://yourdomain.com/product?id=123
    if (uri.scheme == 'https' && uri.host == 'yourdomain.com') {
      String? path = uri.path;
      if (path.startsWith('/product')) {
        String? productId = uri.queryParameters['id'];
        if (productId != null) {
          // Navigate to product details page
          Navigator.push(
            context,
            MaterialPageRoute(
              builder: (context) => ProductDetailsPage(productId: productId),
            ),
          );
        } else {
          // Handle missing product ID
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text('Product ID is missing')),
          );
        }
      } else {
        // Handle other routes or display an error
        print('Unknown route: $path');
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Unknown deep link route')),
        );
      }
    } else {
      print('Invalid deep link: $link');
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Invalid deep link')),
      );
    }
  }


  void dispose() {
    _sub?.cancel();
  }
}

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

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

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

class _MyAppState extends State<MyApp> {
  final _deepLinkingService = DeepLinkingService();

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _deepLinkingService.initialize(context);
    });
  }

  @override
  void dispose() {
    _deepLinkingService.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Deep Linking Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: Text('Home'),
        ),
        body: Center(
          child: Text('Welcome to the app!'),
        ),
      ),
    );
  }
}

Testing Universal Links

To test Universal Links, you can simulate a click on a link in a note, message, or website on a physical device. Make sure that the app is not running when you test the link. Also, the `apple-app-site-association` or `assetlinks.json` file should be correctly set up on your server before you test universal links.

Debugging Deep Linking

Debugging deep linking issues can be challenging. Here are some tips:

  • Check Native Configurations: Ensure your AndroidManifest.xml and Info.plist are correctly configured.
  • Verify Website Setup: Make sure your apple-app-site-association and `assetlinks.json` file is correctly placed and accessible on your website.
  • Use Logging: Add detailed logging to track the deep linking process in your Flutter app.
  • Test on Real Devices: Deep linking behavior can vary on emulators and simulators, so test on real devices.
  • Clear App Data: Sometimes clearing app data or reinstalling the app can resolve deep linking issues.

Conclusion

Deep linking is a powerful tool for improving user experience and engagement in Flutter applications. By implementing custom scheme deep links or Universal Links, you can seamlessly navigate users to specific content within your app from external sources. Understanding the differences between custom schemes and Universal Links, correctly configuring your app and website, and implementing robust error handling are essential for successful deep linking implementation. By following this guide, you can effectively integrate deep linking into your Flutter projects.