InspireIT Logo
InspireIT

Integrate UniLinks with Flutter (Android AppLinks + iOS UniversalLinks)

Step-by-Step Guide

Olá,

Sou o Pedro Dionísio, Flutter Developer de Portugal, na InspireIT. A minha motivação para escrever este tutorial de UniLinks tem por base: InspireIT, and my motto to write this UniLinks tutorial is:

1. O Firebase DynamicLinks está obsoleto e, por recomendação da própria Firebase: não deve mais ser implementado (eu estava a usar e como tinha alguns bugs, comecei a migrar os Deeplink para UniLinks);

2. O método de Deeplink é usado por grandes empresas como TikTok, Instagram, Facebook, etc…

3. Tive alguns problemas a implementar em dispositivos Android (ao tentar abrir e passar dados para o APP)

Então, vou deixar todas as etapas explicadas não só para Flutter Android e iOS, mas também para Flutter Web e Firebase WebHosting. Vamos a isto?

Conceitos Fundamentais

O que é um Deep Linking?
Um Deep Linking é uma espécie de atalho para alguma parte da app.
É um tipo especial de link da web que não apenas abre a app, mas também leva a um local específico dentro da aplicação. É como abrir um livro direto na página que deseja ler.

 

Como funciona?
Digamos que encontraram um artigo incrível numa aplicação e querem partilhar com um amigo. Em vez de enviá-los para a página principal da aplicação e pedir que encontrem o artigo, podem enviar um link que leva diretamente para esse artigo. É como enviar-lhes uma passagem secreta.

 

Qual é a parte fixe?
O incrível é que também é possível enviar instruções ou códigos especiais com este link. Por exemplo, se houver um código de desconto ou uma surpresa escondida na aplicação, pode estar incluído no link. Assim, não chega apenas directamente, mas também com alguns benefícios extras.

 

O que acontece se a aplicação já estiver aberta?
Às vezes, a aplicação já está aberta quando alguém clica no deeplink. Não se preocupem! O deep linking consegue funcionar da mesma forma quando a app está a correr. É como mudar para a página certa de um livro que estão a ler.

 

Algumas notas finais sobre UniLinks
Neste tutorial, eu mostro como podem usar esta ferramenta chamada "uni_links" para fazer DeepLinking de forma super fácil.
É importante dizer que para este tipo de Deeplink é obrigatório ter dois ficheiros de configuração (um para Android e outro para iOS) alocado a um site. O motivo disto é porque estes ficheiros armazenam informação muito importante sobre a aplicação e, com eles, o web browser sabe exatamente para onde redireccionar dentro do seu telemóvel.
Dito isto, vou mostrar-vos como criar um projecto de Flutter Web colocando os ficheiros no lugar correcto.

 

Com calma, é fácil de implementar! Vamos a isto 📱🚀

Criar um projeto de Flutter para a aplicação móvel

Para criar um novo projecto de Flutter basta criar um ficheiro no computando, abrir o CMD dentro desse ficheiro (se abriram um CMD console aleatório, falam o caminho da pasta criada anteriormente) e executar:

flutter create projectname
(Ex: flutter create unilinkproject)

Configurações Android

No projecto: android/app/src/main/AndroidManifest.xml file.
Mudar algumas coisas, começando por substituir: android:launchMode=”singleTop” with android:launchMode=”singleTask” porque apenas queremos aberto um exemplar da nossa aplicação no telemóvel.
Deverá aparecer algo deste género:

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

Depois, no mesmo ficheiro, será necessário configurar a "APP entrance" que será através de um UniLink específico.

Por exemplo, queremos este link para abrir a app: https://mypage.web.app/promos/?promo-id=ABC1 .

Por isso, dentro da atividade vai ser necessário adicionar um intent-filter como este:

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

Configurações iOS

Usando o mesmo exemplo, queremos este link para abrir a app: https://mypage.web.app/promos/?promo-id=ABC1 .
No projecto: ios/Runner/Runner.entitlements ficheiro e seguir key e 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>
				
			

Não é necessário fazer isto mas, se preferirem, podem também fazer esta configuração via XCode:

  • Abrir o Xcode com dois clicks no ios/Runner.xcworkspace file;
  • Ir ao Project navigator (Cmd+1) e selecionar: Runner root item at the very top;
  • Selecionar o Runner target e depois Signing & Capabilities tab;
  • Clicar em + Capability (plus) botão e adicionar a nova capacidade
  • Associar um tipo de domínio e selecionar o item associated domains and select the item;
  • Dois cliques no primeiro item no Domains List e mudar para: webcredentials:example.com to: applinks:mypage.web.app;
  • Um ficheiro chamado Runner.entitlements será criado e adicionado ao projeto

Implementação em Flutter

Normalmente, eu trabalho com uma abordagem modular para fazer tudo de forma organizada mas para este exemplo de projecto vou fazer um mix para fazer tudo mais fácil e intuitivo.

Vamos começar por ter a última versão do uni_links package aqui: https://pub.dev/packages/uni_links e copiar neste projecto: pubspec.yaml desta forma:

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

Guarda e executar flutter pun get para atualizar as dependências do projeto.

Depois disso, adicionar três arquivos de UI: Home Screen, Green Promo Screen e Red Promo Screen.
Ficheiro de Home Screen 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,
          ),
        ),
      ),
    );
  }
}
				
			

Ficheiro de Green Promo Screen 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,
          ),
        ),
      ),
    );
  }
}
				
			

Ecrã Red Promo 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,
          ),
        ),
      ),
    );
  }
}
				
			

Porquê 3 ecrãs? Porque vamos testar em 3 casos:
– Home Screen é mostrado quando a aplicação abre normalmente
– Green Promo Screen é mostrado quando recebemos o Unilink https://mypage.web.app/promos/?promo-id=ABC1;
– Red Promo Screen é mostrado quando recebemos o UniLink https://mypage.web.app/promos/?promo-id=ABC2.

Agora, vamos adicionar um pormenor muito importante que uso sempre nos meus projetos. Com isto, podemos aceder ao mais atualizado: 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;
}
				
			

Depois, vamos adicionar o arquivo responsável por lidar com o 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()),
      );
    }
  }
}
				
			

E, finalmente mudar o nosso ficheiro main.dart para isto:

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

Por aqui, terminamos!
Agora podemos testar abrindo a alicação normalmente para confirmar se o HomeScreen aparece.

Para testar o nosso UniLinks, temos de criar o nosso Flutter Web Project e a Firebase Account e o Projecto. Isto porque precisamos de hospedar a nossa WebApp no Firebase WebHosting.

Criar um Projecto de Flutter Web

Para criar um novo projecto Flutter Web basta criar um ficheiro no computador, abrir CMD dentro desta pasta (se abrir um CMD console random, será necessário fazer o caminho do ficheiro criado anteriormente) e executar:

flutter create projectname
(Ex: flutter create unilinkweb)

Adicionar a HomePage em 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,
            ),
          ],
        ),
      ),
    );
  }
}
				
			

Adicionei 2 ativos para deixar minha página da Web mais incrível, mas se não quiserem, podem excluir as seguintes linhas:

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

Adicionar no main.dart o ficheiro deve parecer-se com isto:

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

Aqui é onde a magia dos UniLinks acontece!
Precisamos de dois ficheiros para fazer a UniLink abrir as nossas aplicações Android e iOS.
Para Android precisamos de adicionar assetlinks.json file no nosso projeto web folder em web/.well-known/assetlinks.json e irá parecer-se com isto:

				
					[
 {
  "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"
   ]
  }
 }
]
				
			

Muito importante: é necessário mudar o package_name e sha256_cert_fingerprints values to your project keys!
Para o Android o package_name no projeto de Flutter Mobile, abrir android/app/build.grade ficheiro e deverá aparecer aqui:

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

Para o Android sha256_cert_fingerprints no projeto Flutter mobile, abrir o terminal e executar cd android para ir ao ficheiro do projecto Android e depois executar ./gradlew signingReportDeverá parecer-se com isto:

Pegar no sha256_cert_fingerprints on SHA-256 key value pair. O meu é AE:34:05:05:27:32… como podem ver.

Para iOS, vamos precisar de adicionar apple-app-site-association ficheiro (não coloquem o .json no final) no ficheiro do projecto web folder em web/.well-known/apple-site-association e irá parecer-se com isto:

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

Para ter appID é necessário juntar o Apple Team ID e o iOS Bundle Identifier como este: teamID.bundleIdentifierSe não existe equipa, podem deixar assim: bundleIdentifier.
Para ter o Bundle Identifier basta ir ao Flutter Mobile Project, abrir ios/Runner.xcodeproj/project.pbxproj file e deverá parecer-se com algo como isto: PRODUCT_BUNDLE_IDENTIFIER = com.example.unilinkproject;.
Para ter o Team ID, basta aceder ao site: https://developer.apple.com/account/#/membership
Isso levará aos detalhes da associação, basta fazer scroll down até ID de equipa e copiar.

E aqui já está tudo pronto!
Agora vamos implementar nosso WebApp no ​​Firebase WebHosting!

Criar Firebase Account & Project

Criar a Firebase Account & Project (caso não tenham um) aqui: https://firebase.google.com/
Basta seguir os passos simples e intuitivos até chegar à página do project console do seu novo projeto. Não dei nenhuma etapa sobre isso porque é um processo muito simples.
A consola do Firebase Project deve parecer-se com isto:

Então, deixo aqui o tutorial do Youtubehttps://youtu.be/A13rZZYbB-Ue/ou podem seguir a minha explicação.
Abra um CMD com privilégios de administrador e instale o Firebase CLI usando npm : npm install -g firebase-tools.
Depois disso, navegue até a raiz do seu projeto assim: cd C:\Users\pedrostick\Desktop\docs\unilinkweb.
Execute firebase login e autentique-se na sua conta do Firebase, seguindo todas as etapas da CLI.
Depois, execute firebase init e irá perguntar: Está pronto para avançar? e responderá sim. y.

Deverá aparecer algo deste género:

Selecionar Hosting: Configurar os ficheiros para o Firebase Hosting e (opcional) set up GitHub Action implementados com with your SPACEBAR tecla do teclado e depois pressione ENTER.

Depois selecione a opção Use an existing project e selecione o Firebase Project criado anteriormente.
Depois disso irá questionar: What do you want to use as your public directory? e responderá sim. build/webDepois seleccione yes y para a single-page app e depois n para compilações e implantações automáticas.

A configuração do Firebase é feita a partir deste ponto e agora, toda vez que quiser implementar uma nova versão do WebApp, precisará construir seu projeto web executando: flutter build web and finally execute firebase deploy to deploy your WebApp.
Irá mostrar o Hosting URL como este:

Como podem ver, o meu é https://unilinkweb-ae13d.web.app portanto, altere o URL UniLink anterior que usamos (https://mypage.web.app/promos/?promo-id=ABC1) pelo URL implementado corretamente (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.

Teste se tudo está correto executando o URL do WebApp hospedado no seu browser assim: https://unilinkweb-ae13d.web.app e teste se os arquivos de configuração do UniLinks estão corretos no Android: https://unilinkweb-ae13d.web.app/.well-known/assetlinks.json Deverá aparecer algo deste género:

E no lado do iOS: https://unilinkweb-ae13d.web.app/.well-known/apple-app-site-association Deverá aparecer algo deste género:

Se tudo estiver correto, estamos prontos para seguir!
Vamos construir a nossa App Mobile e ver se funciona!

Testes

Para testar num dispositivo real, basta clicar no URL que vai redirecionar para a APP instantaneamente, mas, se a APP não estiver instalada, será redireccionado para a WebPage.

Para testar os UniLinks em em emuladores normalmente utilizo um email com todos os links para testar:

Também podem testar utilizando o terminal desta forma:

  • Para Android Emulators executar:
				
					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"'
				
			
  • Para iOS Emulators executar:
				
					/usr/bin/xcrun simctl openurl booted "https://unilinkweb-ae13d.web.app/promos/?promo-id=ABC1"
				
			

Resumo

Para trabalhar com UniLinks precisamos de:
- Configurar AndroidManifest.xml (em Android) e 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 para iOS) num Website hospedado com protocolo seguro https. (Neste tutorial ele foi feito com Flutter Web e hospedado no Firebase Hosting mas pode fazer essa parte diferente se quiser).

E é isso! Se seguirem cada passo com precisão, tudo funcionará corretamente e como desejamos. Simples!

Stay Updated with the Latest in Intelligent Automation
Fique a par das novidades!
Subscreva a nossa newsletter

Latest News and Case Studies