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:
...>
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:
...
...
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:
...
com.apple.developer.associated-domains
applinks:mypage.web.app
...
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 theSigning & 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 _navigatorKey = GlobalKey(debugLabel: 'ContextUtilityNavigatorKey');
static GlobalKey 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 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 _uniLinkHandler({required Uri? uri}) async {
if (uri == null || uri.queryParameters.isEmpty) return;
Map 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: [
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!
