Integrate UniLinks with Flutter (Android AppLinks + iOS UniversalLinks)

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

Step by step guide!

I’m Pedro Dionísio, a Flutter developer at InspireIT in Portugal, and I wrote this UniLinks tutorial with the motto:

  1. Firebase DynamicLinks is deprecated and like Firebase says in its documentation it should no longer be implemented (I was using it and since it had some bugs and was deprecated I decided to start migrating this type of Deeplink to UniLinks );
  2. This Deeplink method is used by big companies like TikTok, Instagram, Facebook…
  3. I’m having some issues implementing it on some specific Android devices (trying to open and pass data to the app).

So, I’m going to make all the steps clear and explain everything, not only for Flutter Android and iOS but also for Flutter Web and Firebase WebHosting so that I don’t miss any step. let’s start!

Deep Linking Introduction

What is Deep Linking?

Deep Linking is like having a shortcut to some part of your application.

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

How does it work?

Let’s say you find a great article in the app and want to share it with your friends. Instead of sending them to the app’s home page and asking them to find the article, you can send them a special link that takes them directly to the article. It’s like giving them a secret passage.

What’s the coolest part?

The cool thing is that you can also send special instructions or a code through this link. For example, if there is a discount code or hidden surprise in the app, you can include it in the link. So, not only do you get to the right place quickly, but you also get some extra benefits.

What happens if the application is already open?

Sometimes, your app may already be open when you click on a deep link. Don’t worry! Deep linking even works when the application is already running. It’s like switching to the correct page in the book you’re reading.

Some final notes about UniLinks

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

It is important that in this type of deep linking, 2 profiles (one for Android and one for iOS) must be assigned in the website. The meaning of this is because these files store important information about your application, and through them, your web browser knows exactly where to redirect within your phone.

With that said, I’m going to show you how to create a Flutter web project and place these files in the right location.

No need to worry at all! This will be easy to implement! let’s start!

Create a Flutter project for your mobile app

Android Configuration

Go to your project’s android/app/src/main/AndroidManifest.xml file.

Here we need to change some things, first replace android:launchMode="singleTop" with android:launchMode="singleTask" because we only want Open an instance of the APP on your phone.

Something like this should appear:

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

Later, in the same file, you need to configure your “APP Portal”, which will be through a specific UniLink.

For example, we want to open the APP through this link: https://mypage.web.app/promos/?promo-id=ABC1.

So, inside the activity, you would add an intent-filter like this:

<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>

iOS Configuration

Using the same example, we want to open the application via this link: https://mypage.web.app/promos/?promo-id=ABC1 .

Go to your project’s ios/Runner/Runner.entitlements file and add the following key and array tags:

<?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>

You don’t need to do this, but you can configure this via XCode if you wish:

  • Double-click the ios/Runner.xcworkspace file to open Xcode;
  • Go to the project navigator (Cmd + 1) and select the topmost Runner root project;
  • Select the Runner target, then select the Signing & Capabilities tab;
  • Click the + Capability (plus sign) button to add a new feature;
  • Enter associated domains and select the project;
  • 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’m going to mix it up and keep 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 like this:

---
dependencies:
  flutter:
    sdk: flutter

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

Save and execute flutter pun get to update your project dependencies.

Then add three user interface files: Home Screen, Green Promotional Screen, and Red Promotional Screen.

Home screen file lib/screens/home_screen.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,
          ),
        ),
      ),
    );
  }
}

Green promo screen file lib/screens/green_promo_screen.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 !!!\\
Code: ${<!-- -->UniLinksService.promoId}",
          textAlign: TextAlign.center,
          style: const TextStyle(
            fontSize: 24,
            fontWeight: FontWeight.bold,
          ),
        ),
      ),
    );
  }
}

Red promo screen lib/screens/red_promo_screen.dart:

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 !!!\\
Code: ${<!-- -->UniLinksService.promoId}",
          textAlign: TextAlign.center,
          style: const TextStyle(
            fontSize: 24,
            fontWeight: FontWeight.bold,
          ),
        ),
      ),
    );
  }
}

Why 3 screens? This is because we want to test 3 situations:

  • The home screen is displayed when the APP is opened normally;
  • When we receive Unilink https://mypage.web.app/promos/?promo-id=ABC1, a green promo screen appears;
  • When we receive UniLink https://mypage.web.app/promos/?promo-id=ABC2, a red promo screen is displayed.

Now let’s add an important utility file that I use frequently in my projects. With it we can access the latest BuildContext anywhere in the APP.

Add this file lib/common/global_context/utils/contect_utility.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;
}

Next we add the file responsible for processing 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 when the application is not running and the user clicks a 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 when the application is already running and the user clicks a 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 opening the APP normally to see if the home screen appears.

Original text: https://medium.com/@pedrostick3/integrate-unilinks-with-flutter-android-applinks-ios-universallinks-c9a1542d6625