Integrate UniLinks with Flutter (Android AppLinks + iOS UniversalLinks)

Let’s integrate UniLinks with Flutter Mobile and Flutter Web.

Step by step guidance!

I’m Pedro Dionísio and I’m a Flutter developer at InspireIT in Portugal, and my motto for writing this UniLinks tutorial is:

  1. Firebase DynamicLinks has been deprecated, as Firebase said in its documentation, and should no longer be implemented (I am using it, and I decided to start migrating this type of Deeplink to UniLinks because it has some errors and has been deprecated);
  2. This Deeplink approach is used by big companies like TikTok, Instagram, and Facebook ……
  3. I’ve had some problems implementing it on certain Android devices (trying to open and pass the data to the app).

So I’m going to make all the steps clear and explain everything, not just for Flutter Android and iOS, but for Flutter Web and Firebase WebHosting, so I don’t miss any steps. Let’s get started!

Deep Linking Introduction

What is Deep Linking?

Deep Linking (Deep Link) is like having a shortcut to some parts of the application.

This is a special network link that not only opens your application, but also takes you to a specific location within the application. Just like opening a book and turning directly to the page you want to read.

How does it work?

Suppose you find a great article in the app and want to share it with your friends. Instead of sending them to the home page of the application and asking them to look up the article, you can send them a special link. It’s like sending them a secret channel.

What is the coolest part?

Most cool, you can also send special instructions or codes through this link. For example, if you have a discount code or hidden surprise in your application, you can include it in the link. So not only do you get to the right place quickly, you get some extra benefits.

What happens if the application is already open?

Sometimes, when you click a deep link, your application may already be open. Don’t worry! Deep links can even work when the application is already running. This is like switching to the correct page in the book you are reading.

Some final notes on UniLinks

In this tutorial, I’ll show you how to make deep linking super simple using a tool called “uni_links.”

Importantly, in this type of deep link, 2 profiles (one for Android and one for iOS) must be assigned to the site. This means that these files store important information about your application and allow your web browser to know exactly where to redirect within your phone.

So I’ll show you how to create a Flutter Web project and put those files in the right place.

Don’t worry! This will be easy to implement! Let’s get started! 📱🚀

Create a Flutter project for your mobile app

Android Configuration

Go to the

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application ...>
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleTask" <!-- <----HERE---- -->
            ...> 
``` file for the project.

Here we need to change something by first replacing `android:launchMode="singleTop"` with `android:launchMode="singleTask"` because we only want to open one instance of the app on the phone.

This should happen:


```xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application ...>
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleTask" <!-- <----HERE---- -->
            ...> 
</code></pre>

Then, in the same file, you need to configure your "app entry," which will take place over a specific UniLink.

For example, we would like to open the app via this link:

<pre data-language=XML><code class="language-markup line-numbers"><manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application ...>
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleTask" <!-- <----HERE---- -->
            ...> 
```.

Therefore, within `activity`, you will add a `intent-filter` as follows:


```xml
<manifest ...>
  <application ...>
    <activity ...>
      ...

      <!-- App Links -->
      <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="mypage.web.app"
          android:pathPrefix="/promos/" />
      </intent-filter>

      ...
    </activity>
  </application>
</manifest> 
</code></pre>

<h4>IOS Configuration</h4>

Using the same example, we would like to open the application through this link:

<pre data-language=XML><code class="language-markup line-numbers"><manifest ...>
  <application ...>
    <activity ...>
      ...

      <!-- App Links -->
      <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="mypage.web.app"
          android:pathPrefix="/promos/" />
      </intent-filter>

      ...
    </activity>
  </application>
</manifest> 
```.

Go to the `ios/Runner/Runner.entitlements` file for the project and add the following `key` and `array` tags:


```shell
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  ...

  <key>com.apple.developer.associated-domains</key>
  <array>
    <string>applinks:mypage.web.app</string>
  </array>

  ...
</dict>
</plist> 
</code></pre>

You do not need to do this, but you can do this through XCode if you like:

<ul>
<li>Double-click the</li>
</ul>

<pre data-language=XML><code class="language-markup line-numbers"><manifest ...>
  <application ...>
    <activity ...>
      ...

      <!-- App Links -->
      <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="mypage.web.app"
          android:pathPrefix="/promos/" />
      </intent-filter>

      ...
    </activity>
  </application>
</manifest> 
``` file to open the Xcode.
*   Go to Cmd+1 and select the topmost `Runner` root item;
*   Select the `Runner` target and then the `Signing & Capabilities` tab;
*   Click the 

```xml
<manifest ...>
  <application ...>
    <activity ...>
      ...

      <!-- App Links -->
      <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="mypage.web.app"
          android:pathPrefix="/promos/" />
      </intent-filter>

      ...
    </activity>
  </application>
</manifest> 
``` (plus sign) button to add a new function.
*   Enter `associated domains` and select the item;
*   Double-click the first item in the domain list and change it from `webcredentials:example.com` to: `applinks:mypage.web.app`;
*   A file named `Runner.entitlements` will be created and added to the project.

### Flutter Implementation

I usually use a modular approach to organizing everything, but for this example project, I will mix and make everything simple and intuitive.

Let's first get the latest version of the uni\_links package here: https://pub.dev/packages/uni\_links and paste it into the project's `pubspec.yaml` file as follows:


```dart
---
dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.2
  uni_links: ^0.5.1 # <---------------- 
</code></pre>

Save and execute <code>flutter pun get</code> to update your project dependencies.

Then add three user interface files: Home, green, and red.

Home screen file

<pre><code class="language-shell line-numbers"><?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  ...

  <key>com.apple.developer.associated-domains</key>
  <array>
    <string>applinks:mypage.web.app</string>
  </array>

  ...
</dict>
</plist> 
```:


```dart
import 'package:flutter/material.dart';

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        alignment: Alignment.center,
        child: const Text(
          "Home Screen",
          style: TextStyle(
            fontSize: 24,
            fontWeight: FontWeight.bold,
          ),
        ),
      ),
    );
  }
} 
</code></pre>

Green Promotion Screen Files

<pre><code class="language-shell line-numbers"><?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  ...

  <key>com.apple.developer.associated-domains</key>
  <array>
    <string>applinks:mypage.web.app</string>
  </array>

  ...
</dict>
</plist> 
```:


```dart
import 'package:flutter/material.dart';
import 'package:unilinkproject/common/uni_links/core/services/uni_links_service.dart';

class GreenPromoScreen extends StatelessWidget {
  const GreenPromoScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        alignment: Alignment.center,
        decoration: const BoxDecoration(
          gradient: LinearGradient(
            colors: [
              Colors.green,
              Colors.greenAccent,
            ],
            begin: Alignment.topRight,
            end: Alignment.bottomLeft,
          ),
        ),
        child: Text(
          "!!! Green Promo !!!\nCode: {UniLinksService.promoId}",
          textAlign: TextAlign.center,
          style: const TextStyle(
            fontSize: 24,
            fontWeight: FontWeight.bold,
          ),
        ),
      ),
    );
  }
} 
</code></pre>

Red promotion screen <code>lib/screens/red_promo_screen.dart</code> :

<pre><code class="language-dart line-numbers">import 'package:flutter/material.dart';
import 'package:unilinkproject/common/uni_links/core/services/uni_links_service.dart';

class RedPromoScreen extends StatelessWidget {
  const RedPromoScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        alignment: Alignment.center,
        decoration: const BoxDecoration(
          gradient: LinearGradient(
            colors: [
              Colors.red,
              Colors.redAccent,
            ],
            begin: Alignment.topRight,
            end: Alignment.bottomLeft,
          ),
        ),
        child: Text(
          "!!! Red Promo !!!\nCode:{UniLinksService.promoId}",
          textAlign: TextAlign.center,
          style: const TextStyle(
            fontSize: 24,
            fontWeight: FontWeight.bold,
          ),
        ),
      ),
    );
  }
} 
</code></pre>

Why 3 screens? This is because we are testing 3 scenarios:

<ul>
<li>The main screen is displayed when the APP is normally opened.</li>
<li>When we receive Unilink https://mypage.web.app/promos/?promo-id=ABC1 , the green promotion screen is displayed;</li>
<li>When we receive UniLink https://mypage.web.app/promos/?promo-id=ABC2 , the red promotion screen is displayed.</li>
</ul>

Now let's add an important utility file that I often use in projects. With it, we can access the latest

<pre><code class="language-dart line-numbers">---
dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.2
  uni_links: ^0.5.1 # <---------------- 
``` anywhere in the app.

Add this file 

```dart
---
dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.2
  uni_links: ^0.5.1 # <---------------- 
```:


```dart
import 'package:flutter/material.dart';

class ContextUtility {
  static final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>(debugLabel: 'ContextUtilityNavigatorKey');
  static GlobalKey<NavigatorState> get navigatorKey => _navigatorKey;

  static bool get hasNavigator => navigatorKey.currentState != null;
  static NavigatorState? get navigator => navigatorKey.currentState;

  static bool get hasContext => navigator?.overlay?.context != null;
  static BuildContext? get context => navigator?.overlay?.context;
} 

Let’s add the file that handles UniLinks lib/common/global_context/utils/context_utility.dart :

import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:uni_links/uni_links.dart';
import 'package:unilinkproject/common/global_context/utils/context_utility.dart';
import 'package:unilinkproject/screens/green_promo_screen.dart';
import 'package:unilinkproject/screens/red_promo_screen.dart';

class UniLinksService {
  static String _promoId = '';
  static String get promoId => _promoId;
  static bool get hasPromoId => _promoId.isNotEmpty;

  static void reset() => _promoId = '';

  static Future<void> init({checkActualVersion = false}) async {
    // This is used in the following situations:Application is not running,User clicks link。
    try {
      final Uri? uri = await getInitialUri();
      _uniLinkHandler(uri: uri);
    } on PlatformException {
      if (kDebugMode) print("(PlatformException) Failed to receive initial uri.");
    } on FormatException catch (error) {
      if (kDebugMode) print("(FormatException) Malformed Initial URI received. Error: error");
    }

    // This is used in the following situations:Application is already running,User clicks link。
    uriLinkStream.listen((Uri? uri) async {
      _uniLinkHandler(uri: uri);
    }, onError: (error) {
      if (kDebugMode) print('UniLinks onUriLink error:error');
    });
  }

  static Future<void> _uniLinkHandler({required Uri? uri}) async {
    if (uri == null || uri.queryParameters.isEmpty) return;
    Map<String, String> params = uri.queryParameters;

    String receivedPromoId = params['promo-id'] ?? '';
    if (receivedPromoId.isEmpty) return;
    _promoId = receivedPromoId;

    if (_promoId == 'ABC1') {
      ContextUtility.navigator?.push(
        MaterialPageRoute(builder: (_) => const GreenPromoScreen()),
      );
    }

    if (_promoId == 'ABC2') {
      ContextUtility.navigator?.push(
        MaterialPageRoute(builder: (_) => const RedPromoScreen()),
      );
    }
  }
} 

Finally, we change the main.dart file to:

import 'package:flutter/material.dart';
import 'package:unilinkproject/common/uni_links/core/services/uni_links_service.dart';
import 'package:unilinkproject/common/global_context/utils/context_utility.dart';
import 'package:unilinkproject/screens/green_promo_screen.dart';
import 'package:unilinkproject/screens/home_screen.dart';
import 'package:unilinkproject/screens/red_promo_screen.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await UniLinksService.init();

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      navigatorKey: ContextUtility.navigatorKey,
      debugShowCheckedModeBanner: false,
      title: 'UniLinks Project',
      routes: {
        '/': (_) => const HomeScreen(),
        '/green-promo': (_) => const GreenPromoScreen(),
        '/red-promo': (_) => const RedPromoScreen(),
      },
    );
  }
} 

We’re done!

You can test whether the home screen appears by opening the APP normally.


This document is transferred from https://blog.csdn.net/duninet/article/details/134210803,If there is any infringement,Please contact to delete。.