InspireIT Logo
InspireIT

Integrate UniLinks with Flutter (Android AppLinks + iOS UniversalLinks)

Step-by-Step Guide

Hello everyone!

I’m Pedro Dionísio, a Flutter developer from Portugal at InspireIT, and my motto to write this UniLinks tutorial is:

1. Firebase DynamicLinks is deprecated and, like Firebase says in their documentation, should not be implemented anymore (I was using it and since it had some bugs and it is deprecated, I decided to start to migrate this type of Deeplink to UniLinks);

2. This Deeplink method is used by big companies like TikTok, Instagram, Facebook, etc…

3. I got some problems implementing it on some specific Android devices (trying to open and pass data to the APP).

So I’ll make it crystal clear with all steps and explain everything, not only for Flutter Android & iOS, but also for Flutter Web and Firebase WebHosting to don’t miss any step. Let’s get into it!

Fundamental Concepts

What is Deep Linking?
Deep linking is like having a shortcut to some part of your app.
It’s a special kind of web link that doesn’t just opens your app but it takes you to a specific spot inside the app too. Like opening a book right to the page you want to read.

 

How Does It Work?
Let’s say you found an awesome article in an app and you want to share it with a friend. Instead of sending them to the app’s main page and asking them to find the article, you can send them a special link that takes them directly to that article. It’s like sending them a secret passageway.

 

What’s the Cool Part?
The cool part is that you can also send special instructions or codes with this link. For example, if there’s a discount code or a 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 goodies.

 

What Happens if the App is Already Open?
Sometimes, your app might already be open when you click on a deep link. No worries! Deep linking can even work when the app is already running. It’s like switching to the right page in a book you’re already reading.

 

Some final notes about UniLinks
In this tutorial, I’ll show you how to use a tool called “uni_links” to make deep linking super easy.
It’s important to say that in this type of deeplink, it is mandatory to have 2 configurations files (one for Android and one for iOS) allocated in a Website. The meaning of this is because those files store important information about your APP and, with them, your web browser knows exactly where to redirect inside your phone.
Saying this, I’m going to show you how to create a Flutter Web project and place those files in the correct place.

 

No worries at all! It will be easy to implement! Let’s get started!📱🚀

Create a Flutter Project for your Mobile APP

To create a new Flutter project just create a folder in your computer, open CMD inside that folder (if you opened a random CMD console, you need to go to the path of your previously created folder) and execute:

flutter create projectname
(Ex: flutter create unilinkproject)

Android Configurations

Go to your project’s android/app/src/main/AndroidManifest.xml file.
Here we need to change a few things, starting by replacing android:launchMode=”singleTop” with android:launchMode=”singleTask” because we only want one instance of our APP open in our phone.
Should appear something like this:

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

After that, in the same file, you need to configure your “APP entrance” that will be via a specific UniLink.

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

So, inside activity you’ll 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 Configurations

Using the same example, we want this link to open the APP: 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 this but, if you prefer, you can do this configuration via XCode too:

  • Open up Xcode by double-clicking on your ios/Runner.xcworkspace file;
  • Go to the Project navigator (Cmd+1) and select the Runner root item at the very top;
  • Select the Runner target and then the Signing & Capabilities tab;
  • Click the + Capability (plus) button to add a new capability;
  • Type associated domains and select the item;
  • Double-click the first item in the Domains list and change it from webcredentials:example.com to: applinks:mypage.web.app;
  • A file called Runner.entitlements will be created and added to the project.

Flutter Implementation

I normally work with a modular approach to make everything organized but for this example project I’ll make a mix to make everything simple and intuitive.

Let’s start by getting the latest version of uni_links package here: https://pub.dev/packages/uni_links and paste it in project’s pubspec.yaml file like this:

				
					...
dependencies:
  flutter:
    sdk: flutter
  
  cupertino_icons: ^1.0.2
  uni_links: ^0.5.1 # <----------------
...
				
			

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

After that add three UI files: Home Screen, Green Promo Screen and Red Promo 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 !!!\nCode: ${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 !!!\nCode: ${UniLinksService.promoId}",
          textAlign: TextAlign.center,
          style: const TextStyle(
            fontSize: 24,
            fontWeight: FontWeight.bold,
          ),
        ),
      ),
    );
  }
}
				
			

Why 3 screens? That’s because we are going to test 3 cases:
– Home Screen is shown when APP opens normally;
– Green Promo Screen is shown when we receive the Unilink https://mypage.web.app/promos/?promo-id=ABC1;
– Red Promo Screen is shown when we receive the UniLink https://mypage.web.app/promos/?promo-id=ABC2.

Now let’s add an important utility file I always use in my projects. With it we can access the most updated BuildContext everywhere 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 to handle 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 for cases when: APP is not running and the user clicks on 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 for cases when: APP is already running and the user clicks on 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()),
      );
    }
  }
}
				
			

And finally we change our main.dart file to this:

				
					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(),
      },
    );
  }
}
				
			

And we’re done here!
You can test opening the APP normally to check if the Home Screen appears.

To test our UniLinks, we need to create a Flutter Web Project and a Firebase Account and Project. That’s because we need to host our WebApp in Firebase WebHosting.

Create Flutter Web Project

To create a new Flutter Web project just create a folder in your computer, open CMD inside that folder (if you openned a random CMD console, you need to go to the path of your previously created folder) and execute:

flutter create projectname
(Ex: flutter create unilinkweb)

Add the Home Page in lib/pages/home_page.dart :

				
					import 'package:flutter/material.dart';
import 'package:unilinkweb/constants/assets_paths.dart';
class HomePage extends StatelessWidget {
  const HomePage({super.key});
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        alignment: Alignment.center,
        decoration: const BoxDecoration(
          gradient: LinearGradient(
            begin: Alignment.topRight,
            end: Alignment.bottomLeft,
            colors: [
              Color(0xffD05E60),
              Color(0xff7354C3),
            ],
          ),
        ),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              "UniLinks APP",
              textAlign: TextAlign.center,
              style: TextStyle(
                fontSize: 30,
                fontWeight: FontWeight.bold,
                color: Colors.white,
              ),
            ),
            const SizedBox(height: 20),
            const Text(
              "If you don't have the app installed, what are you waiting for?\nDownload it now:",
              textAlign: TextAlign.center,
              style: TextStyle(
                fontSize: 20,
                fontWeight: FontWeight.bold,
                color: Colors.white,
              ),
            ),
            const SizedBox(height: 20),
            Image.asset(
              AssetsPaths.APP_STORE_BANNER,
              width: 200,
            ),
            const SizedBox(height: 20),
            Image.asset(
              AssetsPaths.GOOGLE_STORE_BANNER,
              width: 200,
            ),
          ],
        ),
      ),
    );
  }
}
				
			

I’ve added 2 assets to make my Web Page seem cooler but if you don’t want it, you can delete the following lines:

				
					            const SizedBox(height: 20),
            Image.asset(
              AssetsPaths.APP_STORE_BANNER,
              width: 200,
            ),
            const SizedBox(height: 20),
            Image.asset(
              AssetsPaths.GOOGLE_STORE_BANNER,
              width: 200,
            ),
				
			

And in your main.dart file should look like this:

				
					import 'package:flutter/material.dart';
import 'package:unilinkweb/pages/home_page.dart';
void main() {
  runApp(const MyApp());
}
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'UniLinks Web Project',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}
				
			

Here’s where the magic of UniLinks resides!
We need 2 files to make a UniLink open our Android or iOS APPs.
For Android we need to add the assetlinks.json file in our project web folder in web/.well-known/assetlinks.json and it should look like this:

				
					[
 {
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
   "namespace": "android_app",
   "package_name": "com.example.unilinkproject",
   "sha256_cert_fingerprints": [
    "AE:34:05:05:27:32:EB:ED:0B:DC:34:5E:D1:CD:9F:97:97:CD:09:2C:BF:2F:76:35:58:9E:53:5F:4B:E1:9C:91"
   ]
  }
 }
]
				
			

Very Important: You must change the package_name and sha256_cert_fingerprints values to your project keys!
To get the Android package_name go to your Flutter Mobile project, open android/app/build.grade file and it should appear here:

				
					     ...
     defaultConfig {
        applicationId "com.example.unilinkproject"
     ...
				
			

To get the Android sha256_cert_fingerprints go to your Flutter Mobile project, open terminal and execute cd android to go to your project android folder and then execute ./gradlew signingReport. It should appear something like this:

Get the sha256_cert_fingerprints on SHA-256 key value pair. Mine is AE:34:05:05:27:32… as you can see.

For iOS we need to add the apple-app-site-association file (don’t put the .json in the end) in our project web folder in web/.well-known/apple-site-association and it should look like this:

				
					{
 "applinks": {
  "apps": [],
  "details": [{
   "appID": "9B3SD982L3.com.example.unilinkproject",
   "paths": ["/promos/*"]
  }]
 }
}
				
			

To get the appID you need to join your Apple Team ID and your iOS Bundle Identifier like this teamID.bundleIdentifier. If you don’t have a team, you can keep it like this bundleIdentifier.
To get the Bundle Identifier go to your Flutter Mobile project, open ios/Runner.xcodeproj/project.pbxproj file and it should appear like this PRODUCT_BUNDLE_IDENTIFIER = com.example.unilinkproject;.
To get your Team ID, go to this website: https://developer.apple.com/account/#/membership
This will get you to your Membership Details, just scroll down to Team ID and get it.

And here we are all set!
Now let’s deploy our WebApp in Firebase WebHosting!

Create Firebase Account & Project

Create a Firebase Account & Project (if you don’t have one) here: https://firebase.google.com/
Just follow their simple and intuitive steps until you reach the project console page of your new project. I didn’t put any steps about this because it is a very straight forward process.
The console of your Firebase Project should look like this:

Then, you can follow this YouTube tutorial (https://youtu.be/A13rZZYbB-U) or/and you can follow my explanation.
Open a CMD with Admin Privileges and install Firebase CLI using npm : npm install -g firebase-tools.
After that, navigate to your project root like this: cd C:\Users\pedrostick\Desktop\docs\unilinkweb.
Execute firebase login and authenticate to your Firebase account, following all CLI steps.
Then execute firebase init , it will ask Are you ready to proceed? and you’ll type y.

Then it should appear something like this:

Select Hosting: Configure files for Firebase Hosting and (optionally) set up GitHub Action deploys with your SPACEBAR keyboard key and then hit ENTER.

Then select option Use an existing project and select the Firebase Project you created before.
After that it will ask you What do you want to use as your public directory? and you’ll type build/web. Then hit y for single-page app and then n for automatic builds and deploys.

Firebase configuration is done from this point and now, every time you want to deploy a new WebApp version, you’ll need to build your web project executing: flutter build web and finally execute firebase deploy to deploy your WebApp.
It will show you your Hosting URL like this:

As you can see, mine is https://unilinkweb-ae13d.web.app so change the previous UniLink URL that we used (https://mypage.web.app/promos/?promo-id=ABC1) with the correctly deployed one (https://unilinkweb-ae13d.web.app/promos/?promo-id=ABC1) on AndroidManifest.xml (android/app/src/main/AndroidManifest.xml) file and Runner.entitlements (ios/Runner/Runner.entitlements) file.

Test if everything is correct by running your hosted WebApp URL on your browser like this: https://unilinkweb-ae13d.web.app and then test if the UniLinks configuration files are correct on Android side: https://unilinkweb-ae13d.web.app/.well-known/assetlinks.json that should appear like this:

And on iOS side: https://unilinkweb-ae13d.web.app/.well-known/apple-app-site-association that should appear like this:

If everything is correct, we are good to go!
Let’s build our Mobile APP and check if it works!

Tests

To test in a real device, you can simply click the URL and it will redirect to the APP instantly but, if APP it’s not installed, it will redirect you to the WebPage.

To test UniLinks in emulators I normally use an email with all the links to test:

You can also test using the terminal like this:

  • For Android Emulators execute:
				
					adb shell 'am start -W -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "https://unilinkweb-ae13d.web.app/promos/?promo-id=ABC1"'
				
			
  • For iOS Emulators execute:
				
					/usr/bin/xcrun simctl openurl booted "https://unilinkweb-ae13d.web.app/promos/?promo-id=ABC1"
				
			

Resume

To work with UniLinks we need to:
– Configure AndroidManifest.xml (on Android) and Runner.entitlements (on iOS) to open a specific door for UniLink, on mobile APP side.
– Make a handler to listen UniLinks that may appear on mobile APP.
– Deploy 2 configuration files (assetlinks.json for Android and apple-app-site-association for iOS) in a Website hosted with an https secure protocol. (In this tutorial it was made with Flutter Web and hosted in Firebase Hosting but you can do this part different if you want).

And that’s it! If you follow every step precisely, everything will work properly and as you desire to. Simple as that!

Stay Updated with the Latest in Intelligent Automation
Don’t miss anything
Subscribe our newsletter

Latest News and Case Studies