Handling Different Types of Deep Links (URL Schemes, App Links) in Flutter

Deep linking is a powerful technique that allows users to navigate directly to specific content within a mobile application from an external source, such as a web page, an email, or another application. In Flutter, handling deep links involves managing various types of links, including URL schemes and App Links (also known as Android App Links and Universal Links on iOS). Properly implementing deep links enhances user experience and engagement by providing a seamless transition into your app.

Understanding Deep Links

Before diving into implementation details, let’s clarify the types of deep links we’ll be addressing:

  • URL Schemes: Custom URL schemes (e.g., myapp://) allow you to open your app. While easy to implement, they are not secure and can be claimed by other apps.
  • App Links (Android) and Universal Links (iOS): These links use standard HTTP/HTTPS URLs that associate your app with a website you own, providing a more secure and reliable deep linking mechanism.

Why Use Deep Links?

  • Improved User Experience: Direct users to specific content within the app, streamlining their journey.
  • Increased Engagement: Enable easy sharing of app content, driving app usage.
  • Marketing and Promotion: Facilitate tracking and attribution for marketing campaigns.

Implementing Deep Links in Flutter

To handle different types of deep links effectively, we’ll use the uni_links package, which simplifies the process of listening for and parsing incoming links.

Step 1: Add uni_links Dependency

Add the uni_links package to your pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
  uni_links: ^0.5.1

Then, run flutter pub get to install the dependency.

Step 2: Configure URL Schemes

Android Configuration

Open your AndroidManifest.xml file (usually located in android/app/src/main/) and add an <intent-filter> to your <activity> tag to handle your custom URL scheme:

<activity
    android:name=".MainActivity"
    android:launchMode="singleTop"> <!-- Add singleTop launchMode -->
    <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="myapp" android:host="open"/>
    </intent-filter>
</activity>

Here, myapp://open is the custom URL scheme your app will handle. Also ensure that android:launchMode="singleTop" is set to properly handle deep links when the app is already running.

iOS Configuration

Open your Info.plist file (usually located in ios/Runner/) and add a CFBundleURLTypes array to define your custom URL scheme:

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>myapp</string>
        </array>
        <key>CFBundleURLName</key>
        <string>com.example.myapp</string> <!-- Replace with your bundle identifier -->
    </dict>
</array>

Here, myapp is the custom URL scheme for your iOS app. Replace com.example.myapp with your actual bundle identifier.

Step 3: Configure App Links/Universal Links

Android Configuration (App Links)
  1. Associate Your App with Your Website:
  2. Create a assetlinks.json file and host it at /.well-known/assetlinks.json on your domain.

The assetlinks.json file should look like this:

[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.example.myapp",  <!-- Replace with your package name -->
      "sha256_cert_fingerprints": [
        "YOUR_SHA256_CERT_FINGERPRINT"
      ]
    }
  }
]
  • Replace com.example.myapp with your app’s package name.
  • Replace YOUR_SHA256_CERT_FINGERPRINT with the SHA256 fingerprint of your app’s signing certificate. You can generate this using keytool.
keytool -list -v -keystore ./path/to/your/keystore.jks -alias your_alias
  1. Configure AndroidManifest.xml:
  2. Add the following <intent-filter> to your <activity>:
<activity
    android:name=".MainActivity"
    android:launchMode="singleTop"> <!-- Keep singleTop launchMode -->
    <intent-filter android:autoVerify="true"> <!-- Add 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"/> <!-- Replace with your domain -->
        <data android:scheme="http" android:host="yourdomain.com"/>  <!-- Replace with your domain -->
    </intent-filter>

    <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="myapp" android:host="open"/>
    </intent-filter>
</activity>
  • Replace yourdomain.com with your actual domain.
  • The android:autoVerify="true" attribute enables Android to automatically verify that your app is associated with the domain.
iOS Configuration (Universal Links)
  1. Associate Your App with Your Website:
  2. Create an apple-app-site-association file and host it at /.well-known/apple-app-site-association on your domain.

The apple-app-site-association file should look like this:

{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "YOUR_TEAM_ID.com.example.myapp",  <!-- Replace with your Team ID and Bundle Identifier -->
        "paths": [
          "/*"
        ]
      }
    ]
  }
}
  • Replace YOUR_TEAM_ID.com.example.myapp with your Team ID and Bundle Identifier.
  • The paths array specifies which paths on your domain should open your app. /* means all paths.
  1. Configure Your App in Xcode:
  2. Enable Associated Domains in your app’s Xcode project:
  • Go to your project settings, select your target, and then go to the “Signing & Capabilities” tab.
  • Click the “+ Capability” button and add the “Associated Domains” capability.
  • Add your domain with the applinks: prefix to the Associated Domains list (e.g., applinks:yourdomain.com).

Step 4: Handle Incoming Links in Flutter

In your Flutter application, use the uni_links package to listen for incoming links and navigate accordingly.

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

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

class _MyAppState extends State<MyApp> {
  String? _initialUri;
  String? _latestUri;

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

  Future<void> _initUniLinks() async {
    // Platform messages may fail, so we use a try/catch PlatformException.
    try {
      final initialUri = await getInitialUri();
      // Parse the link and update the state
      if (initialUri != null) {
        setState(() {
          _initialUri = initialUri.toString();
          _latestUri = initialUri.toString();
        });
        _handleDeepLink(initialUri);
      }

      // Attach a listener to the stream
      uriLinkStream.listen((Uri? uri) {
        if (uri != null) {
          setState(() {
            _latestUri = uri.toString();
          });
          _handleDeepLink(uri);
        }
      }, onError: (err) {
        setState(() {
          _latestUri = 'Failed to get latest uri: $err.';
        });
      });
    } on PlatformException {
      // Handle exception by warning the user their device isn't supported
      print('Failed to get initial uri.');
    } on FormatException {
      print('Malformed initial uri format.');
    }
  }

  void _handleDeepLink(Uri uri) {
    // Implement your logic to navigate to the appropriate screen
    if (uri.host == 'open' && uri.scheme == 'myapp') {  // For URL Scheme myapp://open
      // Example: myapp://open?param1=value1&param2=value2
      String? param1 = uri.queryParameters['param1'];
      String? param2 = uri.queryParameters['param2'];
      Navigator.push(
        context,
        MaterialPageRoute(builder: (context) => DeepLinkScreen(uri: uri.toString())),
      );
    } else if (uri.host == 'yourdomain.com' && (uri.scheme == 'http' || uri.scheme == 'https')) {  // For App Links/Universal Links
        //Example: https://yourdomain.com/page?id=123
        String? pageId = uri.queryParameters['id'];
        Navigator.push(
          context,
          MaterialPageRoute(builder: (context) => DeepLinkScreen(uri: uri.toString())),
        );
    } else {
      print('Unknown deep link: $uri');
    }
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('UniLinks Example App'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text('Initial URI: $_initialUrin'),
              Text('Latest URI: $_latestUrin'),
            ],
          ),
        ),
      ),
    );
  }
}

class DeepLinkScreen extends StatelessWidget {
  final String uri;

  DeepLinkScreen({Key? key, required this.uri}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Deep Link Content'),
      ),
      body: Center(
        child: Text('You were deep linked to: $uri'),
      ),
    );
  }
}

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

In this code:

  • We initialize uni_links in the initState method.
  • getInitialUri fetches the initial URI, if the app was opened from a deep link.
  • uriLinkStream listens for subsequent deep links.
  • The _handleDeepLink method parses the URI and navigates the user accordingly, providing logic for both URL schemes and App Links/Universal Links.

Testing Deep Links

Testing URL Schemes

On Android, you can use the adb command:

adb shell am start -a android.intent.action.VIEW -d "myapp://open?param1=value1&param2=value2" com.example.myapp

On iOS, you can use the xcrun command (using Simulator):

xcrun simctl openurl booted "myapp://open?param1=value1&param2=value2"

Testing App Links/Universal Links

For Android, you can use the adb command:

adb shell am start -a android.intent.action.VIEW -d "https://yourdomain.com/page?id=123" com.example.myapp

For iOS, simply click the link on a device or simulator to test Universal Links.

Conclusion

Handling deep links effectively in Flutter requires proper configuration for both URL schemes and App Links/Universal Links. Using the uni_links package simplifies listening for and parsing incoming links, while platform-specific configurations ensure that your app is correctly associated with your desired schemes and domains. By following this guide, you can seamlessly integrate deep linking into your Flutter application, improving user experience and driving engagement.