Handling Different Types of Deep Links, Including URL Schemes and Universal/App Links in Flutter

Deep linking is a powerful technique that allows users to navigate directly to a specific section within an app from external sources like websites, emails, or other apps. In Flutter, implementing robust deep linking can significantly enhance user experience and engagement. This blog post will guide you through handling different types of deep links, including URL schemes and Universal/App Links.

What are Deep Links?

Deep links are URIs that direct users to specific content within a mobile app rather than just opening the app’s home screen. There are primarily two types of deep links:

  • URL Schemes: Custom URLs that your app registers to handle (e.g., myapp://content).
  • Universal Links (Android) / App Links (iOS): Standard HTTP URLs that link to both a website and an app. When clicked, the app opens directly without showing an intermediary app chooser.

Why Use Deep Linking?

  • Improved User Experience: Direct users to relevant content instantly.
  • Marketing Campaigns: Track attribution from specific campaigns.
  • Seamless Navigation: Facilitate easy sharing of content within your app.

Setting Up Deep Linking in Flutter

Let’s dive into how to implement deep linking in a Flutter application using both URL schemes and Universal/App Links.

Step 1: Project Setup

Ensure you have a Flutter project set up. If not, create one:

flutter create my_deep_linking_app

Step 2: Adding Dependencies

You’ll need the uni_links package to handle deep links. Add it to your pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
  uni_links: ^0.5.1

Run flutter pub get to install the dependency.

Step 3: Implementing URL Schemes (Custom Schemes)

Android Setup
  1. Open your AndroidManifest.xml file (android/app/src/main/AndroidManifest.xml).
  2. Add an intent filter inside the <activity> tag for your main activity:
<activity
    android:name=".MainActivity"
    android:launchMode="singleTop">
    <!-- Existing metadata and intent filters -->

    <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 is the scheme and open is the host that the app will listen to.

iOS Setup
  1. Open your Info.plist file (ios/Runner/Info.plist).
  2. Add the CFBundleURLTypes array with a dictionary for your URL scheme:
<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>myapp</string>
        </array>
        <key>CFBundleURLName</key>
        <string>com.example.myapp</string>
    </dict>
</array>

Replace com.example.myapp with your app’s bundle identifier and myapp with your URL scheme.

Flutter Code

In your Flutter app, use the uni_links package to listen for incoming 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<MyApp> {
  String? _latestLink = 'Unknown';

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

  Future<void> _initUniLinks() async {
    // Platform messages may fail, so use a try/catch PlatformException.
    try {
      final initialLink = await getInitialLink();
      // Parse the link and update the state
      setState(() {
        _latestLink = initialLink ?? 'Unknown';
      });
    } on PlatformException {
      // Handle exception by warning the user their action did not succeed
      print('Failed to get initial link.');
    }
    
    // Attach a listener to the stream
    linkStream.listen((String? link) {
      setState(() {
        _latestLink = link ?? 'Unknown';
      });
    }, onError: (err) {
      // Handle exception by warning the user their action did not succeed
      print('Failed to get initial link.');
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Deep Linking Example'),
        ),
        body: Center(
          child: Text('The latest deep link is: $_latestLink'),
        ),
      ),
    );
  }
}

This code initializes the listener for incoming links when the app starts and displays the link in the UI.

Step 4: Implementing Universal Links / App Links

Android Setup
  1. Create a assetlinks.json file:
[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.example.myapp",
      "sha256_cert_fingerprints":
      ["XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX"]
    }
  }
]

Replace com.example.myapp with your app’s package name and the SHA256 certificate fingerprint of your signing certificate. Generate the SHA256 fingerprint using the following command:

keytool -list -v -keystore my-release-key.keystore -alias alias_name
  1. Upload the assetlinks.json file:

Host this file at https://yourdomain.com/.well-known/assetlinks.json. Ensure it’s accessible without redirects and that the content type is application/json.

  1. Update AndroidManifest.xml:
<activity android:name=".MainActivity" android:launchMode="singleTop">
    <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="https" android:host="yourdomain.com" />
    </intent-filter>
</activity>

Add the android:autoVerify="true" attribute and update the <data> tag with your domain. Multiple <data> tags can be used for multiple hosts.

iOS Setup
  1. Create an apple-app-site-association file:
{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "TEAMID.com.example.myapp",
        "paths": ["*"]
      }
    ]
  }
}

Replace TEAMID.com.example.myapp with your Team ID and Bundle Identifier.

  1. Upload the apple-app-site-association file:

Host this file at https://yourdomain.com/.well-known/apple-app-site-association and ensure it’s accessible without redirects and the content type is application/json.

  1. Configure Associated Domains in Xcode:
  • Open your Xcode project.
  • Go to your target settings, select “Signing & Capabilities,” and add “Associated Domains.”
  • Add applinks:yourdomain.com to the Associated Domains list.
Flutter Code

The Flutter code to handle the deep link remains the same as with URL schemes:

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<MyApp> {
  String? _latestLink = 'Unknown';

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

  Future<void> _initUniLinks() async {
    // Platform messages may fail, so use a try/catch PlatformException.
    try {
      final initialLink = await getInitialLink();
      // Parse the link and update the state
      setState(() {
        _latestLink = initialLink ?? 'Unknown';
      });
    } on PlatformException {
      // Handle exception by warning the user their action did not succeed
      print('Failed to get initial link.');
    }
    
    // Attach a listener to the stream
    linkStream.listen((String? link) {
      setState(() {
        _latestLink = link ?? 'Unknown';
      });
    }, onError: (err) {
      // Handle exception by warning the user their action did not succeed
      print('Failed to get initial link.');
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Deep Linking Example'),
        ),
        body: Center(
          child: Text('The latest deep link is: $_latestLink'),
        ),
      ),
    );
  }
}

Handling the Deep Link Data

Once you receive the deep link, you need to parse it to extract the necessary data. Here’s how you can modify the Flutter code to handle deep link data:

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<MyApp> {
  String? _latestLink = 'Unknown';
  String? _contentId;

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

  Future<void> _initUniLinks() async {
    try {
      final initialLink = await getInitialLink();
      _parseDeepLink(initialLink);
    } on PlatformException {
      print('Failed to get initial link.');
    }
    
    linkStream.listen((String? link) {
      _parseDeepLink(link);
    }, onError: (err) {
      print('Failed to get initial link.');
    });
  }

  void _parseDeepLink(String? link) {
    if (link != null) {
      Uri uri = Uri.parse(link);
      // Example: Assuming the content ID is in the path segments
      if (uri.pathSegments.isNotEmpty) {
        setState(() {
          _latestLink = link;
          _contentId = uri.pathSegments.first;
        });
      }
    } else {
      setState(() {
        _latestLink = 'Unknown';
        _contentId = null;
      });
    }
  }

  @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('The latest deep link is: $_latestLink'),
              if (_contentId != null)
                Text('Content ID: $_contentId')
              else
                Text('No content ID found'),
            ],
          ),
        ),
      ),
    );
  }
}

In this example, the _parseDeepLink function parses the URI and extracts the content ID from the path segments.

Testing Deep Links

  • URL Schemes:
    • On Android, use the command: adb shell am start -W -a android.intent.action.VIEW -d "myapp://open?content=123" com.example.myapp
    • On iOS, open Safari and type myapp://open?content=123.
  • Universal/App Links:
    • Ensure the assetlinks.json or apple-app-site-association file is correctly set up on your domain.
    • Click on a link (e.g., https://yourdomain.com/content/123) on a device or simulator/emulator.

Conclusion

Deep linking in Flutter enhances user engagement and provides a seamless navigation experience. By implementing both URL schemes and Universal/App Links, you ensure wide compatibility and a robust linking strategy. Proper setup of AndroidManifest.xml, Info.plist, assetlinks.json, and apple-app-site-association files, combined with the uni_links package, provides a comprehensive solution for handling different types of deep links in your Flutter app. Always remember to thoroughly test your deep links on both Android and iOS devices to ensure they work as expected.