Implementing Deep Linking to Navigate Users Directly to Specific Content in Flutter

Deep linking is a powerful mechanism in mobile applications that allows users to navigate directly to specific content within an app from an external source, such as a website, email, or another application. Implementing deep linking in Flutter can significantly enhance user experience by providing seamless access to relevant content. This comprehensive guide covers everything you need to know about implementing deep linking in Flutter, from setting up the necessary configurations to handling different types of deep links.

What is Deep Linking?

Deep linking is the technique of using a uniform resource identifier (URI) that links to a specific location within a mobile app, rather than simply launching the app’s home screen. Deep links are commonly used to direct users to a particular page, product, or feature within an app from external sources like web pages or promotional campaigns.

Why Implement Deep Linking in Flutter?

  • Enhanced User Experience: Deep links provide a seamless user experience by taking users directly to the content they are interested in, reducing friction.
  • Improved Engagement: Directing users to specific parts of your app increases engagement and exploration.
  • Better Conversion Rates: Deep links in marketing campaigns can lead users directly to product pages, boosting conversion rates.
  • Easy Sharing: Allows users to share specific content within the app easily via links.

Types of Deep Links

There are two primary types of deep links:

  • Custom URL Schemes: These use a custom scheme (e.g., myapp://) to open the app. They are easy to implement but less reliable as they depend on the app being installed.
  • Universal Links (Android App Links & iOS Universal Links): These use standard HTTP/HTTPS URLs that resolve both to a website and an app. They are more reliable and secure because they are verified by the operating system.

Setting Up Deep Linking in Flutter

Implementing deep linking in Flutter involves configuring both the Android and iOS platforms and writing the necessary Flutter code to handle the incoming links.

Step 1: Configure AndroidManifest.xml for Custom URL Schemes

To handle custom URL schemes, modify your AndroidManifest.xml file located in android/app/src/main/.


    
        
            
            
            
            
                
                
            
            
            
                
                
                
                
            
        
        
        
        
    

Key parts of this configuration:

  • <intent-filter>: This section defines the intent filter that captures deep links.
  • <action android:name="android.intent.action.VIEW" />: Indicates that this intent filter is for viewing data.
  • <category android:name="android.intent.category.DEFAULT" />: Specifies that the intent filter should be included in the default set of options.
  • <category android:name="android.intent.category.BROWSABLE" />: Allows the app to be launched from a browser.
  • <data android:scheme="myapp" android:host="open" />: Defines the custom URL scheme (myapp) and the host (open). Adjust these values according to your requirements.
  • android:launchMode="singleTop":Ensures that if an instance of the Activity already exists, the new intent is delivered to the existing instance via onNewIntent().

Step 2: Configure Info.plist for Custom URL Schemes

For iOS, configure your Info.plist file located in ios/Runner/ to handle custom URL schemes.


    ...
    CFBundleURLTypes
    
        
            CFBundleURLName
            com.example.myapp
            CFBundleURLSchemes
            
                myapp
            
        
    
    ...

Key points of this configuration:

  • CFBundleURLTypes: Defines the URL schemes that the app can handle.
  • CFBundleURLName: A unique name for the URL type (typically your app’s bundle identifier).
  • CFBundleURLSchemes: An array containing the custom URL scheme (myapp). This scheme should match the one used in your Android configuration.

Step 3: Implement Universal Links (Android App Links)

For Android, implementing Universal Links involves:

  1. Associate Your Website: Create a assetlinks.json file and host it on your website at /.well-known/assetlinks.json.
  2. Configure AndroidManifest.xml: Update your AndroidManifest.xml to handle HTTPS URLs.
Create assetlinks.json

The assetlinks.json file verifies that your app is associated with your website. Here’s an example:

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

To generate the SHA256 certificate fingerprint, use the following command:

keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
Update AndroidManifest.xml for HTTPS URLs

    
    
    
    
    
  • android:autoVerify="true": Enables automatic verification of the app link.
  • <data android:scheme="http" android:host="www.example.com" />: Defines the HTTP URL that the app should handle.
  • <data android:scheme="https" android:host="www.example.com" />: Defines the HTTPS URL that the app should handle.

Step 4: Implement Universal Links (iOS Universal Links)

For iOS, implementing Universal Links involves:

  1. Associate Your Website: Create an apple-app-site-association file and host it on your website.
  2. Configure Associated Domains Entitlement: Add the associated domain to your app’s entitlements.
Create apple-app-site-association File

The apple-app-site-association file verifies that your app is associated with your website. Here’s an example:

{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "YOUR_TEAM_ID.com.example.myapp",
        "paths": ["/open/*"]
      }
    ]
  }
}

Place this file in the .well-known directory of your website (/.well-known/apple-app-site-association). Ensure it is served with the MIME type application/json and is accessible over HTTPS.

Configure Associated Domains Entitlement

In Xcode, navigate to your project settings, select your target, and go to “Signing & Capabilities.” Add the “Associated Domains” capability and add an entry for your domain:

applinks:www.example.com

Flutter Code for Handling Deep Links

Now, let’s look at the Flutter code required to handle the deep links:

Step 1: Add the uni_links Package

First, 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 package.

Step 2: Handle Deep Links in Flutter

Implement the code to listen for incoming deep links and navigate accordingly.

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';
  String? _latestUri;

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

  Future _initUniLinks() async {
    // Platform messages may fail, so use a try/catch PlatformException.
    try {
      final initialLink = await getInitialLink();
      if (initialLink != null) {
        _latestLink = initialLink;
      }
      final initialUri = await getInitialUri();
       if (initialUri != null) {
        _latestUri = initialUri.toString();
      }
    } on PlatformException {
      _latestLink = 'Failed to get initial link.';
      _latestUri = 'Failed to get initial URI.';
    } on FormatException catch (e) {
      _latestLink = 'Failed to parse the initial link as Uri.';
      _latestUri = 'Failed to parse the initial URI.';
      print('Error: $e');
    }

    // Attach a listener to the URI stream
    uriLinkStream.listen((Uri? uri) {
      if (!mounted) return;
      setState(() {
        _latestUri = uri?.toString() ?? 'Unknown';
        _latestLink = uri?.toString() ?? 'Unknown';
      });
        _processDeepLink(uri);
    }, onError: (Object err) {
      if (!mounted) return;
      setState(() {
        _latestUri = 'Failed to get latest URI: $err.';
        _latestLink = 'Failed to get latest Link: $err.';
      });
    });
     setState(() {
          _latestLink = _latestUri ?? 'Unknown';
        });
  }
  
  void _processDeepLink(Uri? uri) {
    if (uri != null) {
      // Parse the URI and navigate to the appropriate screen
      String path = uri.path;
      print('Deep Link Path: $path');

      switch (path) {
        case '/product':
          String? productId = uri.queryParameters['id'];
          if (productId != null) {
            Navigator.push(
              context,
              MaterialPageRoute(
                builder: (context) => ProductDetailsScreen(productId: productId),
              ),
            );
          }
          break;
        case '/profile':
          Navigator.push(
            context,
            MaterialPageRoute(
              builder: (context) => ProfileScreen(),
            ),
          );
          break;
        default:
          print('Unknown deep link: $uri');
          // Handle unknown deep links here (e.g., navigate to the home screen)
          break;
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Deep Linking Example'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text('Initial Link: $_latestLink'),
              Text('Current URI: $_latestUri'),
            ],
          ),
        ),
      ),
    );
  }
}

class ProductDetailsScreen extends StatelessWidget {
  final String productId;

  ProductDetailsScreen({required this.productId});

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

class ProfileScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Profile'),
      ),
      body: Center(
        child: Text('User Profile'),
      ),
    );
  }
}

Explanation:

  • _initUniLinks(): This function initializes deep linking. It retrieves the initial link or URI when the app starts and sets up a listener for incoming links.
  • getInitialLink() and getInitialUri(): These functions fetch the initial deep link or URI that launched the app.
  • uriLinkStream.listen(): This listens for incoming deep links after the app has started. Each time a deep link is received, the callback function is executed.
  • _processDeepLink(): This method parses the URI, extracts the path and query parameters, and navigates to the corresponding screen within the app.
  • Navigation: Based on the deep link, use Navigator.push() to navigate to the appropriate screen.

Testing Deep Links

To test deep links, you can use the following commands:

Android

Using ADB (Android Debug Bridge):

adb shell am start -W -a android.intent.action.VIEW -d "myapp://open?product_id=123" com.example.myapp

For Universal Links, use:

adb shell am start -W -a android.intent.action.VIEW -d "https://www.example.com/open?product_id=123" com.example.myapp

iOS

Using xcrun:

xcrun simctl openurl booted "myapp://open?product_id=123"

For Universal Links, use:

xcrun simctl openurl booted "https://www.example.com/open?product_id=123"

Best Practices for Deep Linking

  • Handle Errors: Always handle potential errors, such as invalid deep links or network issues.
  • Provide Fallbacks: Implement fallback mechanisms to navigate users to a relevant screen if the deep link is malformed or unsupported.
  • Use HTTPS: Always use HTTPS for Universal Links to ensure security and prevent interception.
  • Test Thoroughly: Test deep links on different devices and OS versions to ensure compatibility.
  • Analytics Tracking: Integrate analytics to track the performance of deep links and understand user behavior.

Conclusion

Implementing deep linking in Flutter is essential for enhancing user experience and engagement. By configuring both Android and iOS platforms and using the uni_links package, you can seamlessly direct users to specific content within your app. Whether you choose to use custom URL schemes or Universal Links, deep linking can significantly improve your app’s usability and effectiveness. Remember to follow best practices to ensure robust and secure deep link handling.