Firebase

Présentation du cours

Présentation personnelle

  • ancien de l'ESGI : Architecture des Logiciels
  • spécialités : développement Flutter et Android natif
  • anciennes expériences : Agence de dev mobile
  • maintenant :
    • Founder @ Flappy
    • formateur : Flutter, Android, Git, Algo, Firebase
    • organisateur du Meetup Flutter Paris
       
  • adresse mail : thomasecalle+prof@gmail.com

Présentation des règles

  • les retards
  • l'attention en cours
  • les supports de cours
  • la notation
    • un cas pratique
    • un projet à faire en équipe
       
  • je prends vos feedbacks !

Prérequis

  • connaissances en développement d'application mobiles (notions de mise en production, "flavors/scheme", etc.)
     
  • connaissances en Flutter

Petit point sur le choix de Flutter comme technologie

Jusqu'où irons-nous

  • cours d'initiation de 15h = non experts
  • nous allons quand même pouvoir voir de nombreux outils Firebase (mais pas tous !) :
    • découverte de l'outil en général
    • Analytics
    • Bases de données temps réel (Realtime et Firestore)
    • Firebase Authentication
    • Crashlytics
    • Cloud Messenging
    • Cloud Functions

Firebase

Qu'est-ce que c'est ?

Mini historique

  • lancement en 2011 sous le nom d'Envolve
  • rachat par Google en Octobre 2014

Voilà... je vous ai pas menti sur le côté "mini" de l'historique.

Une boîte à outils Cloud

Vous pouvez voir Firebase comme une grosse boîte à outils cloud.

Créer un projet Firebase, c'est avoir accès à de nombreux outils cloud utilisables sur n'importe quel type d'application (mobile ou non, d'ailleurs).

Une boîte à outils Cloud

  • Build
    outils de management de backend, de machine learning, d'hébergement, etc.
     
  • Release & Monitoring
    Store d'applications, outils de testing, outils d'analyse de crashs, etc.
     
  • Engage
    Envoie de notifications ciblées, A/B testing, mise en place de segments utilisateurs, etc.

Pourquoi l'apprendre ?

Firebase est devenu indispensable dans la vie d'un développeur d'applications mobiles !

Il y a fort à parier que vous ayez déjà eu affaire à au moins l'un de ses outils pour une application mobile

Comprendre son fonctionnement, l'utilité des différents outils, leur utilisation / pertinence, leur implication sur un vrai projet, etc. est essentiel

Vraiment indispensable ?

Oui ... et non.

Un développeur mobile se doit de comprendre encore une fois ses rouages et de savoir utiliser ses outils à bon escient

MAIS, la sur-utilisation de Firebase peut également être un problème sur le long terme !

(anecdote personnelle sur une appli avec sur-utilisation de Firebase)

Vraiment indispensable ?

Il faut savoir utiliser Firebase avec pertinence. Parfois nous en avons besoin, parfois non. Comprendre la pertinence des outils et savoir quand les utiliser ou non peut faire une vraie différence !

C'est l'objectif de ce cours :)

Créer un projet Firebase

Projet Firebase

Pour commencer à jouer avec Firebase, RDV sur la :

Projet Firebase

Votre console Firebase est liée à votre compte Gmail.

C'est peut-être un détail lorsqu'on débute, mais faites bien attention à créer un projet lié à l'entreprise X dans la console firebase liée au compte Gmail de l'entreprise X

Projet Firebase

Par exemple, ma Console Firebase thomasecalle@gmail.com regroupe mes petits tests persos

Pour chaque projet "pro", je crée mon projet Firebase dans la Console associée

Exemple avec ma startup COMO associée au gmail como-app

Projet Firebase

Ca peut sembler être un détail, mais ça structure beaucoup votre projet et vous pourrez regrettez plus tard de ne pas avoir pensé à ça 

Exemple pour une Agence :

  • on crée le projet sur la console du client
  • on s'ajoute en administrateur
  • comme ça, si on arrête de bosser ensemble, le client a FULL accès à la console et peut nous virer

Projet Firebase

Ok, maintenant que nous avons vu ça... c'est quoi, un projet Firebase ?

Un projet Firebase représente un projet d'application

Si on reprend notre exemple de boîte à outils, c'est donc la boîte à outils associée au projet X

Firebase utilise le terme de "Conteneur" cloud pour vos applications

Projet Firebase

Dans un projet, vous allez pouvoir enregistrer à chaque fois une application iOS, une Android et une WEB

En théorie du coup, chaque application mobile différente est un nouveau projet Firebase !

Pourquoi en théorie ?

Projet Firebase

Lorsqu'on développe une application mobile sérieuse, nous mettons souvent en place des "saveurs / scheme" ("saveurs" étant la traduction pour flavors)

D'ailleurs, petit aparté, si vous ne le faites pas... eh bien faites le ! 

Gérer des saveurs différentes pour une appli mobile devient indispensable pour une application mobile professionnelle

Projet Firebase

Quel rapport avec un projet Firebase ?

Lorsque vous créer des saveurs pour vos apps, ça signifie souvent que vous allez gérer différents identifiants (package name côté Android & Bundle ID côté iOS)

Ca signifie que ce sont vraiment des applications différentes !

Projet Firebase

Lorsqu'on enregistre une application dans un projet Firebase, on indique l'identifiant de celle-ci, car Firebase sait que celui-ci est UNIQUE et donc s'assure qu'il pourra spécifiquement cibler celle-ci

Ainsi, vous allez souvent avoir autant de projets Firebase que de saveurs :

  • un projet DEV avec les apps de dev
  • un projet PROD avec les apps de prod
  • etc.

Projet Firebase

Ca fait partie des petits détails qui changent tout lorsqu'on passe d'applications faites à l'arrache à des applications professionnelles

Le tout étant de bien comprendre tout ça pour ne pas prendre de mauvaises décisions

Projet Firebase

Dernier "détail", et pas des moindre, avant de créer notre premier projet :

Un projet Firebase, c'est un projet Google Cloud

Projet Firebase

En fait, lorsque vous créez un projet Firebase, ça vous crée en arrière plan un projet Google Cloud

Voyez Firebase comme une interface sexy donnant accès à tous les services Google Cloud + des configurations et outils spécifiques à Firebase

Projet Firebase

Pourquoi c'est important à comprendre ?

Parce que, du coup, c'est Google derrière !

C'est parfois un frein pour certaines entreprises et c'est votre rôle de développeur de comprendre les conséquences de tels choix et d'être capable de les expliquer

Projet Firebase

D'ailleurs, d'autres acteurs proposent des concurrents

C'est le cas surtout d'AWS Amplify, la solution proposée par Amazon

Bien que ça n'ait pour le moment pas autant d'impact que Firebase, c'est important de l'avoir à l'oeil et de s'y intéresser

Projet Firebase

Il y a également des concurrents Open Source !

Projet Firebase

Peut-être que certains de ces outils vous conviendront mieux que Firebase

Peut-être que Firebase se fera un jour remplacer par l'équivalent Amazon ou un autre

Mais, aujourd'hui, Firebase est leader sur le marché et il est important d'y prêter attention en tant que dévelopeur mobile

Projet Firebase

OK.

Maintenant, on peut créer notre premier projet Firebase :

Projet Firebase

On donne d'abord le nom de notre projet

Observez l'identifiant généré. C'est lui qui doit être unique

Projet Firebase

Ajouter (ou non) Google Analytics à notre projet 

Projet Firebase

Ajouter Google Analytics n'est pas anodin

L'idée derrière étant de récupérer pas mal d'informations sur la navigation de l'utilisateur dans votre application, il faut que vous ayez conscience des répercussions que ça a par rapport à la RGPD

Cependant, c'est souvent bien utile

Projet Firebase

Vous devrez ensuite lier ce projet Firebase à un compte Google Analytics

Il est tout à fait possible de vouloir lier plusieurs projets à un même compte ou encore que votre client/projet ait déjà un compte existant que vous pourrez lier

Projet Firebase

Sinon, vous pouvez en créer un

Projet Firebase

Et voilà !

Tour d'horizon

Tour d'horizon

Sur le panneau de gauche, vous trouverez l'ensemble des services proposés par Firebase

Tour d'horizon

Sélection du projet

Lier une application (iOS, Android ou web) au projet

Tour d'horizon

Paramètre généraux du projet

Firebase, c'est payant ?

Firebase, c'est payant ?

EVIDEMMENT !

Tous ces services sont hébergés et entièrement gérés par les infrastructures de Google

Comme tous les services Cloud du genre, il va falloir payer

Firebase, c'est payant ?

MAIS

Une très grosse partie peut être entièrement gratuite

En fait, vous avez 2 plans possible :

  • Spark (totalement gratuit mais limité)
  • Blaze ("pay as you go", avec gratuité sur les limites du Spark)

Firebase, c'est payant ?

Beaucoup de services sont gratuits quoi qu'il arrive :

  • Analytics
  • A/B Testing
  • App Distribution
  • Crashlytics
  • Cloud Messaging
  • Dynamic Links
  • etc.

Pour les autres services, ça dépend de votre mode :

Firebase, c'est payant ?

Spark

En mode Spark, vous êtes sûr que tout est gratuit et que vous ne serez jamais débité de quoi que ce soit

Il y a évidemment des limites d'utilisations propres à chaque service

Firebase, c'est payant ?

Spark

Par exemple, pour l'authentification par numéro de téléphone, vous serez limités à 10K d'authentification avec succès par mois

Firebase, c'est payant ?

Blaze

Le mode Blaze est défini comme "Pay as you go"

On peut difficilement faire plus clair ! En gros, vous payerez les différents services en fonction de leur utilisation !

Firebase, c'est payant ?

Blaze

En reprenant l'exemple de l'authentification par téléphone, on est à 0.06$ la vérification (en France, le prix peut-être différent pour les US, le Canada et l'Inde)

Firebase, c'est payant ?

Blaze

Attention, avec Blaze, vous gardez ce qu'offre Spark !

Par exemple, toujours pour l'authentification par téléphone, les 10K premières vérifications restent gratuites 

Firebase, c'est payant ?

Alors faut-il payer ?

Comme vous pouvez le voir, les limites de Spark sont quand même très larges !

Vous pouvez à priori largement débuter sur Spark et ne passer en Blaze que lorsque vous voyez que votre application prend de l'ampleur

Firebase, c'est payant ?

Attention cependant

Le service de Cloud Functions par exemple n'est pas accessible avec Spark

Il a tout de même une grosse partie gratuite, mais son activation nécessitera quand même de passer sur Blaze

Et les Cloud Functions, si c'est mal géré... ça peut coûter cher (petite anecdote)

Firebase, c'est payant ?

Si vous voulez tester un peu tout ça, Google propose un programme de 300$ de crédits disponible sur 90 jours

Créons notre application

Créons notre application

Maintenant que le projet Firebase est prêt, on va pouvoir lier nos applications iOS & Android à celui-ci

La première étape est évidemment de créer une application mobile

Nous partirons dans ce cours sur la technologie Flutter

Pourquoi Flutter ?

  • permet de voir les config sur Android ET iOS
     
  • parce que vous avez eu normalement cours de Flutter en 3ème année MOC
     
  • parce qu'il faut bien choisir une techno, et que Flutter c'est le 🔥

Créons notre application

Attention, pour la suite de ce cours, nous nous baserons sur la version la plus récente sur la channel Stable d SDK  Flutter

Soit, à l'heure où j'écris ces lignes, la version 2.0.3

L'important étant surtout que la version soit supérieure à 2.0, cette version ayant changé pas mal de choses 

Si vous n'êtes pas à jour :

flutter upgrade

Créons notre application

Commençons donc par créer notre application, avec Android Studio (ou VSCode) ou directement en ligne de commande

Créons notre application

Une fois que c'est bon, nous allons pouvoir la lier à notre projet Firebase

On lie séparément l'application Android et l'application iOS car ce sont bien 2 applications complètement différentes

Créons notre application

Attention, pour mettre en place le null-safety, on va upgrade la version de Dart requise en passant de :

Créons notre application

environment:
  sdk: ">=2.7.0 <3.0.0"

à :

environment:
  sdk: ">=2.12.0 <3.0.0"

Lier l'application Android

Lier l'application Android

Commençons par Android :

Ici, seul le package Android est obligatoire. Vous le trouverez dans le build.gradle (app) de votre application

Lier l'application Android

Le certificat de signature SHA-1n'est pas obligatoire et pourra être modifié par la suite

Cependant, il est intéressant pour certains services afin que ceux-ci puissent générer une clé d'API propre à votre application

Lier l'application Android

Nous allons quand même le renseigner pour voir comment faire

Pour ce faire, rendez-vous à la racine de votre projet puis lancez la commande suivant :

cd android && ./gradlew signingReport && cd ../

Lier l'application Android

Cette commande va généré le rapport de signatures de votre application Android

Il ne vous reste plus ensuite qu'à sélectionner le SHA-1 correspondant à votre mode de build d'APK

Lier l'application Android

(petit aparté sur le mode de Build)

Lorsque vous buildez votre application Android, vous avez 3 modes : debug, profile et release

En release, Flutter va utiliser le mode de compilation AOT permettant de largement améliorer les performances et va également signer votre APK avec votre keystore etc.

Lier l'application Android

En profile, Flutter va utiliser le mode de compilation AOT permettant de largement améliorer les performances mais ne va pas signer votre APK

Enfin, en debug, Flutter va compiler en JIT pour permettre le couple Hot Reload / Hot Restart et ne signera pas votre APK

Lier l'application Android

Pourquoi ces précisions ?

Parce que chaque mode de compilation génère des certificats de signature SHA-1différents !

Debug et Profile vont avoir une clé SHA-1 différente de Release, il faut donc bien prendre les 2 si on veut que les services marchent à la fois en debug et en release

Lier l'application Android

(Petit aparté sur la signature auto-gérée par le PlayStore)

Ok, on peut continuer !

Lier l'application Android

Cette étape est la plus importante !

Lier l'application Android

Sur Android comme sur iOS, Firebase va vous demander d'ajouter un fichier de configurations dans vos fichiers

Sur Android, il s'agira du : google-services.json

Sur iOS, le : GoogleService-info.plist

Lier l'application Android

Ces fichiers sont OBLIGATOIRES

En fait, voyez les comme la carte d'identité de votre application mobile par rapport au projet Firebase associé

Ces fichiers vont en effet contenir toutes les clés publiques d'API et autres informations nécessaire à Firebase pour identifier le bon projet Firebase

Lier l'application Android

Ce qui signifie que des modifications dans les settings de votre app au niveau du projet Firebase peuvent devoir entraîner une nouvelle génération du fichier (changement de package name, ajout de SHA-1, etc.)

C'est très important de comprendre ça

Lier l'application Android

De plus, vous vous rappelez de la discussion sur les flavors et la gestion de plusieurs projets Firebase ?

Il ne faudra donc pas oublier d'avoir plusieurs fichiers de configurations, un par flavor

Super important aussi ça

Lier l'application Android

On télécharge donc le fichier, et on le mets dans le dossier android/app

Si on avait plusieurs flavors, dev, preprod et prod par exemple, on mettrait les différents fichiers dans :

  • android/app/src/dev
  • android/app/src/preprod
  • android/app/src/prod

Lier l'application Android

Ensuite, ils ne nous reste plus qu'à déclarer et appliquer le plugin google-services dans notre Gradle

Lier l'application Android

Plus tard dans votre utilisation de Firebase, il est probable que vous ayez une erreur de type 

Error while merging dex archives: 
The number of method references in a .dex file cannot exceed 64K

Lier l'application Android

C'est fini pour Android !

Vous pouvez passer les étapes suivantes sur la doc et retourner ensuite sur votre projet Firebase dans votre console

Lier l'application Android

Lier l'application iOS

Lier l'application iOS

Lier l'application iOS

Lier l'application iOS

Ici, c'est pareil que sur Android, la seule information nécessaire est le Bundle Id de votre application !

Lier l'application iOS

Lier l'application iOS

On va devoir ensuite télécharger le fichier de confs pour iOS

Lier l'application iOS

Attention, pour ajouter le fichier, et s'assurer qu'il soit ajouté au project, il va falloir le faire via XCode

En effet, l'ajouter manuellement ne suffira pas

Du coup... pour ceux qui n'ont pas de Mac... vous ferez ça une fois que vous aurez accès à un Mac ! Merci Apple !

Lier l'application iOS

Lier l'application iOS

Lier l'application iOS

Et voilà !

Attention que le nom soit bien exempt de tout (1) ou autre trucs ajoutés par votre OS si vous en aviez téléchargé plusieurs

Lier l'application iOS

On est OK pour iOS !

Vous pouvez passer la suite des indications, qui ne sont pas nécessaires pour une intégration Flutter mais le sont pour du natif

Nouveautés Flutter 3

Nouveautés Flutter 3

Le 11 mai 2022, Flutter 3 a été annoncé à la Google I/O !

Au programme :

  • flutter MacOS en stable avec beaucoup de composants macOS
  • écrans pliables supportés
  • améliorations performances iOS
  • amélioration build iOS
  • amélioration gestion des images et du splashscreen en Flutter WEB
  • améliorations performances Dart + enums avec paramètres
  • Material 3 +
  • etc.

Nouveautés Flutter 3

Quel rapport avec Firebase ?

Les relations entre Flutter et Firebase ne font que s'améliorer car Flutter devient enfin une cible de base pour Firebase :

Nouveautés Flutter 3

Google a ainsi voulu améliorer l'expérience developpeur lors de la mise en place de Firebase sur un projet Flutter

L'idée est de s'abstraire de la récupération à la main des fichiers de configurations, etc.

Nouveautés Flutter 3

Pour cela il faudra au préalable télécharger l'utilitaire Firebase en ligne de commandes :

S'y connecter via la commande 

firebase login

Nouveautés Flutter 3

Ensuite, il n'y a plus qu'à suivre les étapes, et hop, tout est fait pour vous au niveau de la gestion des configurations :)

Nouveautés Flutter 3

Pour initialiser Firebase, on met en place le code qu'ils nous proposent (en récupérant firebase_core) de cette manière :

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

import 'firebase_options.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
  runApp(const MyApp());
}

Et maintenant ?

Et maintenant ?

Je vous l'accorde, c'était chiant.

Mettre en place le projet Firebase, télécharger les fichiers, les mettre dans les dossiers, gérer potentiellement X flavors... c'est chiant.

Mais c'est nécessaire pour commencer à travailler avec Firebase ! Et une fois que tout est en place, on peut commencer à s'amuser

Et maintenant ?

N'oubliez pas que ces étapes seront potentiellement à refaire si vous mettez en place plusieurs saveurs dans votre application avec du coup des dossiers spécifiques pour vos fichiers de configuration, des SHA-1 et des identifiants différents, etc.

Et maintenant ?

On peut enfin passer à la suite !

A partir de maintenant, vous trouverez des exemples de code complets dans le GitHub que j'ai crée pour vous : 

Et maintenant ?

Chaque "chapitre" donnera lieu à un fichier Dart que vous pourrez RUN

Attention cependant : je n'ai pas versionné mes fichiers de configuration Firebase !

Du coup, ça vous oblige (si vous voulez tester) à y mettre les votre ! Mais ça vous permet surtout d'adapter ce code à n'importe quel projet Firebase :)

Firebase : des services / des dépendances

(j'avais pas trop d'idée de titre j'avoue)

Dépendances Firebase

On l'a déjà expliqué mais Firebase c'est un ensemble d'outils Cloud

Chaque outil évolue à sa vitesse et est maintenue par des équipes spécifiques chez Firebase/Google

Firebase a donc séparé les différents services en autant de dépendances différentes

Dépendances Firebase

Evidemment, les versions des dépendances et leurs implémentations changent selon les technologies

Ici nous nous basons donc sur Flutter, mais sachez que la logique générale est exactement la même à chaque fois, seule la syntaxe et l'implémentation change éventuellement

Dépendances Firebase

Si il y a une seule dépendance que vous aurez toujours, quoi qu'il arrive, c'est firebase_core

C'est le "coeur" de Firebase qui va nous permettre de lier notre app au projet Firebase

dependencies:
  flutter:
    sdk: flutter
  firebase_core: "1.2.1"

Dépendances Firebase

Quelle est la différence entre Legacy et Null safety ?

Tous les packages Firebase ne sont pas encore passés en mode null-safety !

Dépendances Firebase

Pour certains services, si vous voulez récupérer la dépendance null-safety, il vous faudra vous mettre sur la channel Beta de Flutter

A priori, c'est pas un grand risque, mais je vais pour ma part me baser sur la version Stable au cas où dans le contexte de ce cours

Dépendances Firebase

MAIS DU COUP

Il va falloir préciser à flutter que vous ne souhaitez pas compiler en mode "sound null-safety" car sinon il refusera vos dépendances Legacy Firebase

Initialisation firebase_core

Initialisation firebase_core

Avant toute chose, il va falloir initialiser Firebase pour pouvoir l'utiliser dans votre application

Il est impératif de le faire avant d'utiliser le moindre service

await Firebase.initializeApp();

Initialisation firebase_core

Vous pouvez par exemple vous baser sur l'exemple proposé par FlutterFire :

import 'package:flutter/material.dart';

// Import the firebase_core plugin
import 'package:firebase_core/firebase_core.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(App());
}

class App extends StatelessWidget {
  // Create the initialization Future outside of `build`:
  final Future<FirebaseApp> _initialization = Firebase.initializeApp();

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      // Initialize FlutterFire:
      future: _initialization,
      builder: (context, snapshot) {
        // Check for errors
        if (snapshot.hasError) {
          return SomethingWentWrong();
        }

        // Once complete, show your application
        if (snapshot.connectionState == ConnectionState.done) {
          return MyAwesomeApp();
        }

        // Otherwise, show something whilst waiting for initialization to complete
        return Loading();
      },
    );
  }
}

Initialisation firebase_core

Ou alors... beaucoup plus simple :

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(
    AnalyticsManager(
      analytics: FirebaseAnalytics(),
      child: MaterialApp(
        home: Home(),
      ),
    ),
  );
}

Firebase Analytics

Firebase Analytics

Pour commencer, on ajoute la dépendance

dependencies:
  flutter:
    sdk: flutter
  firebase_core: version.recente
  firebase_analytics: version.recente

Firebase Analytics

Attention, sur Android il va sûrement falloir mettre le minSDK à 19 et sur iOS, dans le PodFile, la platform à 10

Firebase Analytics

Et voilà !

Rien qu'en mettant la dépendance, beaucoup d'évènements seront loggés sur votre Dashboard

Firebase Analytics

Firebase Analytics va vous permettre de logger des évènements ainsi que des user properties

Les deux portent plutôt bien leurs noms :

  • un évènement, c'est un log que l'on veut effectuer lorsque l'utilisateur effectue une action particulière (navigue d'un écran à l'autre, clique sur un bouton, etc.)
  • une User Property, c'est une information permettant de catégoriser l'utilisateur en question

Firebase Analytics

Les Users Properties vont nous permettre de créer des Audiences d'utilisateurs comme par exemple : "les hommes de plus de 25 ans qui ont un abonnement premium"

Combiner les User Properties aux Évènements permet d'avoir des informations très précises sur l'utilisation de votre application par les utilisateurs et ainsi de mieux les comprendre et de mieux les cibler par la suite (re-targetting par notifications, etc.)

Firebase Analytics

Attention, je ne peux que vous déconseiller GRANDEMENT le fait de stocker dans les User Properties des informations personnelles (permettant de l'identifier) de l'utilisateur comme son nom ou adresse email

Côté RGPD, ça serait très compliqué et Google ne le recommande vraiment pas

Anonymiser les données ne vous empêche pas d'avoir quand même des audiences très précises et qualitatives !

Firebase Analytics

Maintenant qu'on a compris l'intérêt des évènements et des user properties, comment faire pour les logger ?

Pour le coup, ce n'est pas très compliqué !

Firebase Analytics

Ici, par exemple, nous loggons un évènement nommé "buy_product"

final FirebaseAnalytics analytics = FirebaseAnalytics();
analytics.logEvent(
	name: 'buy_product', 
	parameters: {'price': 10},
);

Notez que vous pouvez donner une Map de paramètres spécifique permettant de personnaliser l'évènement en question

Firebase Analytics

Ici, par exemple, nous loggons une User Property avec "role" pour clef et "admin" comme valeur

final FirebaseAnalytics analytics = FirebaseAnalytics();
analytics.setUserProperty(
	name: 'role',
	value: 'admin',
);

Firebase Analytics

Pas très compliqué n'est-ce pas ?

Du côté de FirebaseAnalytics, vous pouvez en faire un Singleton ou utiliser le pattern InheritedWidget plus conforme à la manière de penser de Flutter

Firebase Analytics

class AnalyticsManager extends InheritedWidget {
  const AnalyticsManager({
    Key? key,
    required Widget child,
    required this.analytics,
  }) : super(key: key, child: child);
  final FirebaseAnalytics analytics;

  static AnalyticsManager of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType(aspect: AnalyticsManager)!;
  }

  @override
  bool updateShouldNotify(AnalyticsManager e) => false;

  void logEvent({required String name, Map<String, dynamic>? parameters}) {
    analytics.logEvent(name: name, parameters: parameters);
  }
}

Par exemple :

Firebase Analytics

Pour pouvoir l'instancier ainsi :

  await Firebase.initializeApp();
  runApp(
    AnalyticsManager(
      analytics: FirebaseAnalytics(),
      child: MaterialApp(
        home: Home(),
      ),
    ),
  );

(pour faire encore plus propre, préférez passer une interface à AnalyticsManager et donc FirebaseAnalytics pour plus de maintenabilité et resistance au changement)

Firebase Analytics

Et l'utiliser comme ça :

class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Center(
          child: ElevatedButton(
            child: Text("Click to log 'press_event'"),
            onPressed: () {
              AnalyticsManager.of(context).logEvent(name: "press_event");
            },
          ),
        ),
      ),
    );
  }
}

Firebase Analytics

Et voilà !

Par contre, ça peut mettre du temps avant que vos User Properties ou vos évènements apparaissent sur le DashBoard, pour des soucis de performances et d'optimisation réseau.. (jusqu'à 24h parfois)

Firebase Analytics

Mais on peut quand même tester :)

Pour ce faire, on va lancer la "Debug View"

Firebase nous permet d'avoir une vue en direct des évènements loggés pour pouvoir tester que tout fonctionne bien

Firebase Analytics

Pour ce faire, on doit l'activer côté Android :

adb shell setprop debug.firebase.analytics.app com.example.exampleapp

(pour le désactiver ensuite :)

adb shell setprop debug.firebase.analytics.app .none.

Firebase Analytics

Une fois que c'est fait, direction la Debug View sur votre projet Firebase :

Firebase Analytics

Si votre téléphone n'apparaît pas immédiatement... re-tentez, relance l'app, etc. ça bug parfois un peu honnêtement...

En tout cas, on est bon côté Firebase Analytics ! :)

Firebase Analytics

Petit exercice :

Créer une application qui affiche 3 boutons sur la Home qui permettent d'aller sur 3 écrans différents

Logger les évènements de navigation sur Analytics

Firebase Analytics

Petite correction avec l'observer de Navigation disponible sur le dépôt GitHub du cours :

Firestore -présentation

Firestore - présentation

Si vous avez déjà eu affaire à l'un des services de Firebase, je suis presque certain que c'est pour l'une de ses Bases de Données

En effet, Firebase s'est rendu très célèbre pour son concept de BDD "realtime client-first SDK"

Si nous devions résumer/traduire "realtime client-first SDK", ça donnerait :

  • une BDD temps réel (pas besoin de recharger l'appel)
  • on communique directement depuis le SDK mobile
  • aucun serveur à maintenir ou à avoir
  • performances et scalabilité au rendez-vous
  • gestion de la persistence Offline

Firestore - présentation

Il existe en fait 2 services distincts de BDD qui sont :

  • Firebase Realtime Database
  • Firestore

Les 2 services sont des BDD NO-SQL avec les caractéristiques citées précédemment

Firestore - présentation

Petit aparté sur le NO-SQL

Pour vous, c'est quoi ?

Firestore - présentation

Le NO-SQL, ça n'a rien de nouveau ou de révolutionnaire

Une technologie de BDD NO-SQL est simplement une solution de stockage de données qui n'est pas basée sur le SQL

En effet, le SQL n'est parfois pas adapté à certaines problématiques et de nombreuses solutions NO-SQL sont apparues pour pallier ce problème

Firestore - présentation

MAIS

Le SQL reste la solution la plus pertinente dans de très nombreux cas

De plus, chaque solution NO-SQL est basé sur un stockage différent et certaines solutions sont orientées sur des problématiques précises (exemple des Graphes pour les réseaux sociaux)

Firestore - présentation

Pourquoi je précise ça ?

Parce que le NO-SQL a connu un vrai boum et certains y ont vu "une révolution" alors qu'il s'agit seulement d'autres technos

Certains développeurs font parfois le choix du NO-SQL "parce que c'est quand même mieux que ce vieux SQL"...... et ce genre de choix basé sur des arguments aussi nuls peut avoir de lourdes conséquences !

Firestore - présentation

Comparer le NO-SQL au SQL, c'est comme comparer 2 langages ou 2 Frameworks

Prendre en compte le contexte, les besoins spécifiques de votre application et des arguments logiques et scientifiques sera TOUJOURS une meilleure philosophie que de prendre "ce qui a l'air d'être le plus cool"

Firestore - présentation

BREF

Firestore - présentation

Une fois qu'on a vu tout ça.. quelles sont alors les différences entre Firestore et Realtime Database et comment choisir l'une ou l'autre des BDD ?

Au delà du fait que Firestore est plus récent, il existe un quiz sur la documentation de Firebase pour savoir quelle technologie choisir :

Firestore - présentation

A savoir aussi que les données ne sont pas du tout stockées sous le même format NO-SQL :

  • Realtime Database -> Gros document JSON
  • Firestore -> collections de données

Parce que c'est la solution la plus récente, qui permet de faire des requêtes plus fines, de structurer plus de données, etc. ...

Firestore - présentation

Nous étudierons dans ce cours Firestore !

Mais n'hésitez pas à vous renseigner sur Realtime Database

Firestore

Firestore

Commençons par activer le service associé dans notre console Firebase :

Firestore

Choisissez ensuite le mode test pour commencer :

Firestore

Quésako ?

Firestore est une BDD sur laquelle vous allez pouvoir envoyer des requêtes d'écritures ou de lecture depuis votre application directement : il n'y a pas d'API, vous écrivez et lisez directement dessus

Pour ce faire, vous allez simplement devoir faire référence à votre base de données.. et c'est bon ! Et ça c'est cool !

Firestore

Mais du coup, n'importe qui qui aurait cette référence, pourrait écrire ou lire ce qu'il veut dans votre base de données.... et ça c'est moins cool

C'est pour ça qu'on mettra ensuite en place des règles de sécurité sur notre BDD, mais dans un premier temps mettons nous en "mode test"

Firestore

On sélectionne ensuite la zone où nous souhaitons que les données soient stockées

Ce choix ne pourra pas être modifié donc faites-y attention

Firestore

Tout est OK niveau console Firebase !

Firestore

Ajoutons maintenant la dépendance côté Flutter pour pouvoir utiliser Firestore :

cloud_firestore: "2.2.1"

Et normalement.. ça devrait être bon !

Firestore

Pour tester que tout fonctionne bien, nous allons tenter notre première écriture sur la base de données ! 

Je vous expliquerai plus tard le fonctionnement plus en détail, là on test juste que la connexion se fait avec un petit exemple

Firestore

Faites n'importe quelle écran avec un bouton qui lance cette méthode :

Future<void> _addUser() async {
    final CollectionReference usersCollection = FirebaseFirestore.instance.collection('users');

    try {
      await usersCollection.add({"first_name": "John", "last_name": "Doe", "age": 42});
      print("User added");
    } catch (error) {
      print("Failed to add user: $error");
    }
  }

Firestore

Lancez votre application et cliquez sur le bouton, cet utilisateur a dû être renseigné dans votre Firestore !

Un problème lors de la compilation ?

Ca parle de "dex" ne pouvant exceder 64k de lignes de codes, etc. ?

On en a déjà parlé, je vous laisse voir comment régler ça dans le chapitre sur la liaison avec Android ;)

Firestore

Problème de lenteur dans le XCode Build côté iOS ?

Firestore

Avant d'aller plus loins, tâchons de comprendre de quelle manière les données sont structurées dans Firestore 

Dans Firestore, nous avons des Collections qui sont des listes de Documents

Chaque Document est un objet (un peu comme en JSON) stocké au format clé/valeur et qui peut lui-même contenir des sous-collections, etc.

Firestore

Si on reprend l'exemple que vous avez développé, ça donne ça :

Firestore

Notez que Firestore crée des identifiants par défauts à vos documents mais que vous pourriez potentiellement en mettre un vous-même

Firestore

Côté création/suppression de collections :

La création du premier Document dans une Collection créera celle-ci de même que la suppression du dernier Document supprimera celle-ci 

 MAIS attention :

La suppression d'un Document qui comporte des sous-collections ne suppriment pas celles-ci ! Vous devrez alors les supprimer vous-même

Firestore

Pour connaître tous les types possible pour un champs d'un Document, je vous laisse voir ça

Firestore

Écrire sur la BDD

Comme nos l'avons vu dans notre exemple, la première étape est de récupérer une référence dans la BDD

Une référence de Collection par exemple :

final CollectionReference collectionReference = FirebaseFirestore.instance.collection('users');

Firestore

Écrire sur la BDD

Une fois qu'on a une référence, nous pouvons ajouter un élément à cette collection ainsi :

final DocumentReference ref = await collectionReference.add({"key1": "value1", "key2", "value2"});

L'identifiant de l'élément sera alors automatiquement généré par Firestore et accessible via ref.id

Firestore

Écrire sur la BDD

Pour avoir notre propre identifiant, nous pouvons remplacer l'appel à add par set, comme ceci :

await collectionReference.doc("MONID").set({"key1": "value1", "key2", "value2"});

Firestore

Écrire sur la BDD

Enfin, il est possible d'écrire en profondeur dans la BDD :

Future<void> _addDummyData() async {
    final CollectionReference ref = FirebaseFirestore.instance.collection('users/unidentifiant/friends');

    try {
      await ref.add({"first_name": "John", "last_name": "Doe", "age": 42});
      print("Friend added");
    } catch (error) {
      print("Failed to add user: $error");
    }
  }

Firestore

Modifier un Document

Pour modifier un Document, nous avons la méthode update qui peut être utiliser ainsi par exemple :

await collectionReference.doc("MONID").update({"first_name": "NewValue"});

Firestore

Lire la BDD

Lorsque vous demandez à lire des données depuis Firestore, vous avez la possibilité de le faire de 2 manières :

  • en mode one-time 
  • en mode realtime

Firestore

Lire la BDD

Lorsque nous allons lire une donnée, nous allons récupérer une référence sur l'élément puis récupérer une Future ou un Stream (selon le one-time ou realtime) avec le DocumentSnapshot ou le QuerySnapshot associé

Firestore

Lire la BDD

Un DocumentSnapshot, c'est l'objet qui encapsule un Document

On récupère la Map des données du Document via sa méthode data

Attention à vérifier son existence via la méthode exists car un snaphot sera toujours renvoyé sans forcément de datas

Firestore

Lire la BDD

Un QuerySnapshot, c'est l'objet qui encapsule une Collection

On y retrouve sa taille et d'autres métadata ainsi que la liste des DocumentSnapshot via le champs docs

Firestore

Lire la BDD - one-time

En mode "one-time", nous allons utiliser la méthode get pour récupérer une Future

final CollectionReference users = FirebaseFirestore.instance.collection('users');

// Pour un Document
final DocumentSnapshot snapshot = await users.doc("MONID").get();

// Pour une Collection
final QuerySnapshot snapshot = await users.get();

Firestore

Lire la BDD - realtime

En mode "realtime", nous allons utiliser la méthode snapshots pour récupérer un Stream

final CollectionReference users = FirebaseFirestore.instance.collection('users');

// Pour un Document
final Stream<DocumentSnapshot> stream = users.doc("MONID").snapshots();

// Pour une Collection
final Stream<QuerySnapshot> stream = users.snapshots();

Firestore

Lire la BDD -  queries

Il est possible de mettre en place pas mal de finesse dans la récupération des données de votre BDD

Firestore

Lire la BDD -  queries

On peut mettre

Firestore

Lire la BDD -  queries

On peut également mettre en place de la pagintation !

final Query first = FirebaseFirestore.instance.collection('cities').orderBy("population").limit(25);

final QuerySnapshot firstCities = await first.get();

// ...
// On affiche notre liste graphiquement, etc.

// Lorsqu'on affiche le dernier élément, on récupère sa référence

final DocumentSnapshot lastVisible = firstCities.docs[firstCities.size - 1];

// ...
// Et on peut ensuite récupérer les 25 prochaines par exmeple

final Query second =
	FirebaseFirestore.instance
	.collection('cities')
	.orderBy("population")
	.startAfterDocument(lastVisible)
	.limit(25);

Firestore

Suppressions

Pour supprimer des données, rien de plus simple :

Future<void> deleteUser() async {
	final CollectionReference users = await FirebaseFirestore.instance.collection('users');
	try {
		await users.doc('ABC123').delete();
		print("User Deleted");
	} catch(error) {
		print("Failed to delete user: $error");
	}  
}

Firestore

Les transactions

Imaginez que vous ayez un compteur qui peut être incrémenté par plusieurs utilisateurs en même temps (un nombre d'abonnés pour une chaîne YouTube par exemple)

Il peut arriver que plusieurs utilisateurs décident de s'abonner en même temps à la chaîne

Firestore

Les transactions

Comment s'assurer que le compteur est bien incrémenté de 2 et non pas de 1 dans le cas où vous récupériez la valeur, l'incrémenteriez puis la réécririez ?

Pour ça, on peut utiliser les Transactions, qui assurent que la valeur prise en compte sera celle présente sur le serveur au moment de l'écriture et non pas celle que vous récupérez en lecture de votre côté

Firestore

Les transactions

Exemple proposé par la documentation :

// Create a reference to the document the transaction will use
DocumentReference documentReference = FirebaseFirestore.instance
  .collection('users')
  .doc(documentId);

return Firestore.instance.runTransaction((transaction) async {
  // Get the document
  DocumentSnapshot snapshot = await transaction.get(documentReference);

  if (!snapshot.exists) {
    throw Exception("User does not exist!");
  }

  // Update the follower count based on the current count
  // Note: this could be done without a transaction
  // by updating the population using FieldValue.increment()

  int newFollowerCount = snapshot.data()['followers'] + 1;

  // Perform an update on the document
  transaction.update(documentReference, {'followers': newFollowerCount});

  // Return the new count
  return newFollowerCount;
})
.then((value) => print("Follower count updated to $value"))
.catchError((error) => print("Failed to update user followers: $error"));

Firestore

Dans le but de toujours plus faciliter la vie des développeurs, Firebase a mis en place une bibliothèque de composants UI déjà tout fait pour certains cas, par exemple pour afficher facilement une liste "infinite scroll" ou encore pour faciliter les login, etc.

Firestore

Petit exercice

Faire une petite application qui affiche une liste d'utilisateurs provenant de notre BDD Firestore

On aura un bouton flottant qui créera un utilisateur à chaque fois qu'on cliquera dessus

La liste doit automatiquement être mise à jour lorsque l'utilisateur est créé

Crashlytics

Crashlytics

Si il y a bien UN outil de la suite Firebase qu'il est INDISPENSABLE d'implémenter dans votre application, c'est celui-ci

Crashlytics est un service permettant de capter les différentes exceptions que vous ne traitez pas dans votre app et vous les affiche dans un Dashboard sur votre console Firebase

Crashlytics

Penser, en tant que développeur, qu'on a réussi à prévoir tous les bugs possibles, toutes les utilisations possibles de notre application, tous les Devices imaginables des utilisateurs, tous les cas chelous... c'est être soit idiot, soit inconscient.

Il y aura FORCÉMENT des crashs, des erreurs imprévues, etc.

Crashlytics

Grâce à Crashlytics, nous allons pouvoir analyser ceux-ci, voir leur StackTrace et donc les corriger, les marquer comme résolu et voir par la suite si ils reviennent ou non, analyser leur apparition selon les modèles de téléphones ou encore leur version d'OS, etc.

Crashlytics

N'importe quelle application mise en prod DOIT avoir une solution de reporting de crashs comme Crashlytics, l'inverse serait vraiment inconscient

Crashlytics

Déclarons donc la dépendance :

firebase_crashlytics: "version.recente"

Nous avons quelques étapes de plus côté Android & iOS pour mettre en place la dépendance

Ancienne façon

Crashlytics

Android :

dans le fichier android/build.gradle, ajoutez :

dependencies {
  // ... other dependencies
  classpath 'com.google.gms:google-services:4.3.5'
  classpath 'com.google.firebase:firebase-crashlytics-gradle:2.5.1'
}

Ancienne façon

Crashlytics

Android :

dans le fichier android/app/build.gradle, ajoutez :

apply plugin: 'com.google.firebase.crashlytics'

Ancienne façon

Crashlytics

iOS :

Côté iOS, on va devoir ajouter un script dans les Build Phases sur XCode

Une fois de plus, si vous n'avez pas XCode, concentrez-vous sur Android

Ancienne façon

Crashlytics

iOS :

  1. sur XCode, sélectionnez votre Runner
  2. allez sur Build Phases puis + > New Run Script Phase
  3. Ajoutez le code suivant dans le "Type a script"
${PODS_ROOT}/FirebaseCrashlytics/run

Ancienne façon

Crashlytics

Enfin, il nous reste à activer Crashlytics sur la console Firebase

Attention, il faut l'activer pour Android ET pour iOS

Ancienne façon

Crashlytics

Une fois le service ajouté, voici ce qui apparaît :

En fait, Firebase attend de nous que nous fassions un premier "Crash de test" pour lancer le service

Ancienne façon

Crashlytics

Nouveau

Désormais, tout est plus simple :

Attention à bien lancer le 

flutterfire configure

qui va venir changer les choses pour nous dans le build.gradle etc.

Crashlytics

Nous allons donc faire ça mais, avant, nous allons écrire le code nécessaire pour la remontée des bugs côté Flutter

Crashlytics

Tout d'abord, nous allons demander à Flutter de reporter toutes les erreurs non traitées dans Crashlytics :

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  await Firebase.initializeApp();

  // Pass all uncaught errors from the framework to Crashlytics.
  FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterError;

  runApp(MyApp());
}

Crashlytics

Ensuite, nous allons également devoir entourer notre main dans une méthode spécifique comme ceci :

void main() {
  runZonedGuarded<Future<void>>(() async {
    // The following lines are the same as previously explained in "Handling uncaught errors"
    WidgetsFlutterBinding.ensureInitialized();
    await Firebase.initializeApp();
    FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterError;

    runApp(MyApp());
  }, FirebaseCrashlytics.instance.recordError);
}

Crashlytics

Enfin, toujours avant de run notre App, nous allons ajouter ceci :

void main() {
  runZonedGuarded<Future<void>>(() async {
   
   	//... le reste ...
    
    Isolate.current.addErrorListener(RawReceivePort((pair) async {
      final List<dynamic> errorAndStacktrace = pair;
      await FirebaseCrashlytics.instance.recordError(
        errorAndStacktrace.first,
        errorAndStacktrace.last,
      );
    }).sendPort);

    runApp(MyApp());
  }, FirebaseCrashlytics.instance.recordError);
}

Crashlytics

En conclusion, voilà la tête que devrait avoir la méthode main par rapport à l'implémentation de Crashlytics :

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();

  runZonedGuarded<Future<void>>(() async {
    FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterError;

    Isolate.current.addErrorListener(RawReceivePort((pair) async {
      final List<dynamic> errorAndStacktrace = pair;
      await FirebaseCrashlytics.instance.recordError(
        errorAndStacktrace.first,
        errorAndStacktrace.last,
      );
    }).sendPort);

    runApp(MaterialApp(home: Home()));
  }, FirebaseCrashlytics.instance.recordError);
}

Crashlytics

C'est bon ! On va enfin pouvoir tester !

Attention cependant, dans la doc de FlutterFire, on vous propose de tester un faux crash grâce à cette instruction :

FirebaseCrashlytics.instance.crash();

Crashlytics

Ça ne fonctionne en revanche pas toujours, je vous conseille donc de lancer une bonne vielle Exception du style : 

void _crash() {
    throw Exception("WAHOU, voici un beau test de bug !");
  }

Crashlytics

Allez-y, faites le test, tout devrait être bon :)

Attention, les crashs ne sont reportés que lors du prochain redémarrage de l'application

Une fois le crash provoqué, quittez l'app, relancez là, et voilà :

Crashlytics

Pour aller plus loin, on peut également logger nos propres exceptions non-fatales :

    try {
      // On teste quelque chose
      throw Exception("Aie, quelque chose s'est mal passé");
    } catch (error, stacktrace) {
      await FirebaseCrashlytics.instance.recordError(
          error,
          stacktrace,
          reason: 'a non-fatal error'
      );
    }

Messaging

Messaging

Grâce à Firebase Cloud Messaging, nous allons pouvoir envoyer des notifications ciblées à nos utilisateurs !

Ce sont les fameuses "notifications push" dont nous avons déjà parlé plus tôt

Messaging

Pour ce faire, déclarons la dépendance :

firebase_messaging: "version.recente"

Côté iOS, il reste un certain nombre de choses à faire, propre aux signatures des applications iOS, que vous trouverez 

Messaging

Ensuite, il est important de comprendre que les applications ont 3 états différents possibles : 

  • Foreground : l'application est ouverte et utilisée
  • Background : Ouverte mais en background
  • Terminated : Téléphone verrouillé ou app fermée

Messaging

Il faudra demander la permission à l'utilisateur pour pouvoir lui envoyer des notifications

Pour ce faire, voici un example de code :

FirebaseMessaging messaging = FirebaseMessaging.instance;

NotificationSettings settings = await messaging.requestPermission(
  alert: true,
  announcement: false,
  badge: true,
  carPlay: false,
  criticalAlert: false,
  provisional: false,
  sound: true,
);

print('User granted permission: ${settings.authorizationStatus}');

Messaging

Le AuthorizationStatus peut avoir alors différentes valeurs :

  • authorized : c'est OK, l'utilisateur a accepté
  • denied : l'utilisateur a refusé
  • notDetermined : l'utilisateur n'a pas choisi
  • provisional : autorisation provisoire

Messaging

En fait, côté Android, le statut sera toujours Authorized

Cependant l'instruction est tout de même obligatoire, mettez là donc pour les 2 OS :

Messaging

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(MaterialApp(home: Home()));
}

class Home extends StatefulWidget {
  @override
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> {
  @override
  void initState() {
    super.initState();
    _initFirebaseCloudMessaging();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: Center(child: Text("Un magnifique écran pour recevoir des notifations")),
    );
  }

  void _initFirebaseCloudMessaging() async {
    FirebaseMessaging messaging = FirebaseMessaging.instance;

    NotificationSettings settings = await messaging.requestPermission(
      alert: true,
      announcement: false,
      badge: true,
      carPlay: false,
      criticalAlert: false,
      provisional: false,
      sound: true,
    );

    print('User granted permission: ${settings.authorizationStatus}');
  }
}

Messaging

OK, on peut faire nos premiers tests, direction la console Firebase > Cloud Messaging, et lancez votre première notification !

Attention, le comportement par défaut est que la notification n'apparaîtra dans le centre de notifications que si l'application est en Background ou Terminated

Messaging

Ce comportement est dû au fait que vous n'avez peut-être pas envie de la même expérience si l'utilisateur est justement en train d'utiliser l'application

Il est toutefois parfaitement possible de se mettre à l'écoute de telle notifications et de réagir à leur réception

Messaging

Pour ce faire, nous écoutons les notifs comme ceci :

FirebaseMessaging.onMessage.listen((RemoteMessage message) {
      print('Got a message whilst in the foreground!');
      print('Message data: ${message.data}');

      if (message.notification != null) {
        print('Message also contained a notification: ${message.notification?.title}');
      }
    });

Messaging

Vous pouvez de nouveau tester votre notif en gardant votre application en Foreground, et vous arriverez à l'afficher dans vos logs 

Messaging

C'est cool, mais comment faire si on veut que notre notification s'affiche dans le centre de notifications, même si l'app est en Foreground ?

Côté iOS, c'est plutôt simple :

 await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions(
      alert: true, // Required to display a heads up notification
      badge: true,
      sound: false,
    );

Messaging

Côté Android, c'est légèrement plus tricky

On l'a vu juste avant : on arrive à récupérer les infos de la notification

Ce qu'on va vouloir faire maintenant, c'est utiliser un package spécifique (flutter_local_notifications) pour afficher une notification locale lorsque nous recevons la notification push en mode Foreground (relisez cette phrase 2-3 fois, mais promis ça veut dire quelque chose)

Messaging

Ok, déclarons d'abord la dépendance :

flutter_local_notifications: "5.0.0+4"

(Attention, ne prenez pas au dessus de 6.0.0 si vous êtes à une version de Flutter < 2.2)

Messaging

Ensuite, il nous faut initialiser le package 

void _initLocalNotifications() async {

    final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = 
    FlutterLocalNotificationsPlugin();
    
    const AndroidInitializationSettings initializationSettingsAndroid =
        AndroidInitializationSettings('@mipmap/ic_launcher');

    final InitializationSettings initializationSettings = InitializationSettings(
      android: initializationSettingsAndroid,
    );

    await flutterLocalNotificationsPlugin.initialize(
      initializationSettings,
      onSelectNotification: _onNotificationClicked,
    );
  }

Messaging

Puis, lors de la réception d'une notification en Foreground, nous allons utiliser le package pour :

  • créer une Channel avec une forte priorité
  • envoyer une notification locale sur cette Channel

Messaging

Voici le code correspondant que je vais vous expliquer :

FirebaseMessaging.onMessage.listen((message) => _onMessage(message, onForeground: true));
void _onMessage(RemoteMessage message, {bool onForeground = false}) async {
    final AndroidNotification? android = message.notification?.android;
    final RemoteNotification? notification = message.notification;

    if (notification != null && android != null && onForeground) {
      const AndroidNotificationChannel channel = AndroidNotificationChannel(
        'high_importance_channel',
        'High Importance Notifications',
        'This channel is used for important notifications.',
        importance: Importance.max,
      );

      final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();

      await flutterLocalNotificationsPlugin
          .resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
          ?.createNotificationChannel(channel);

      flutterLocalNotificationsPlugin.show(
        notification.hashCode,
        notification.title,
        notification.body,
        NotificationDetails(
          android: AndroidNotificationDetails(
            channel.id,
            channel.name,
            channel.description,
            icon: android.smallIcon,
          ),
        ),
      );
    }
  }

Messaging

On est bon ! Vous pouvez tester !

Désormais, lorsqu'une notification est reçue en Foreground, sur Android, vous lancerez l'équivalent et elle apparaîtra dans le centre de notifications du téléphone

Messaging

Nous voilà déjà bien avancés, mais on va voir encore quelques trucs :

  • savoir quand une notification a ouvert l'app
  • récupérer le Token de l'appareil
  • enregistrer des Devices de test côté Firebase

Messaging

Parfois, votre application sera ouverte depuis une notification, c'est le comportement de base lorsque celle-ci est cliquée

Pour être au courant d'un tel comportement, vous pouvez implémenter cette callback :

FirebaseMessaging.onMessageOpenedApp.listen(_onNotificationOpenedApp);
void _onNotificationOpenedApp(RemoteMessage message) async {
    print("Une notification a ouvert l'application ! ${message.notification?.title}");
  }

Messaging

Ensuite, nous allons nous intéresser au FCM Token

Il faut savoir que chaque Device possède un FCM Token qui lui est propre

Cet identifiant permet de cibler un ou plusieurs téléphones lorsque nous souhaitons envoyer une notification

Messaging

Grâce à ce Token, nous pourrons faire 2 choses intéressantes :

  • déclarer des Devices de test sur Firebase
  • l'envoyer à notre API pour que celle-ci puisse, à certaines occasions, envoyer des notifications aux utilisateurs

Messaging

Pour récupérer le Token, ça se passe comme ça :

void _lookForMessagingToken() async {
    String? token = await FirebaseMessaging.instance.getToken();
    print("Firebase Messaging Token : $token");
    FirebaseMessaging.instance.onTokenRefresh.listen((String token) {
      print("Firebase Messaging Token : $token");
    });
  }

Il est possible que Firebase rafraîchisse le Token (même si c'est rare), c'est pourquoi nous nous abonnons à ses changements

Messaging

Et... voilà !

Si vous regardez dans vos logs, vous avez maintenant le Token correspondant à votre Device !

À vous de voir ce que vous voulez en faire par rapport à votre API, moi je vais vous montrer comment s'en servir sur Firebase

Messaging

Ça se passe ici :

Messaging

Ça se passe ici :

Renseignez votre Token, et c'est bon !

Remote Config

Remote Config

Le principe derrière Remote Config est d'une facilité déconcertante :

Firebase stocke un fichier Json sur ses serveurs et vient le récupérer à chaque fois que vous lancez l'app... pour vous fournir ce qu'il contient

Là, dis comme ça, ça paraît super con

Remote Config

Dans un premier temps : OUI, c'est très "simple" et c'est quelque chose qu'on pourrait faire faire par notre API

Mais :

  • on a pas forcément d'API (Firebase c'est sensé être Serverless justement)
  • on a pas forcément accès à cette API (pas la même équipe)
  • etc.

Remote Config

Mais aussi :

Remote config permet de ne propager les données qu'à certains utilisateurs ! Ca permet de mettre en place de l'AB testing assez facilement

Et ça... c'est déjà plus compliqué à mettre en place sur une API perso

Remote Config

Qu'est-ce qu'on met dans un Remote Config ?

  • des données A/B testables
     
  • des données qu'on veut pouvoir ajouter/retirer sans mise à jour (bouton spécifique)
     
  • des cas d'urgences (popup bloquante)

Remote Config

Allez, c'est parti, on le met en place :

firebase_remote_config: "^0.10.0+1"

Remote Config

Ok, ensuite, on va récupéré les informations du serveur :

final bool updated = await RemoteConfig.instance.fetchAndActivate();
  if (updated) {
    // the config has been updated, new parameter values are available.
  } else {
    // the config values were previously updated.
  }

Le code est plutôt clair mais en gros : updated est true si de nouvelles valeurs ont été récupérées et faux sinon

Remote Config

Mettons en place 2 configurations :

Remote Config

minBuildNumber :

Numéro de version minimale que doit avoir l'utilisateur, sinon on affiche la popup bloquante

blackButton :

Par exemple ici, si c'est true, on fait un certain design (noir) sinon on en fait un autre

Remote Config

Bien sûr, il faut ensuite publier les changements !

Remote Config

OK, maintenant on récupère les infos côté app, et on en fait quelque chose ! 

Par exemple avec le booléen :

final RemoteConfig remoteConfig = RemoteConfig.instance;
final bool updated = await remoteConfig.fetchAndActivate();
final bool blackButton = remoteConfig.getBool("blackButton");

Remote Config

Remote Config

ElevatedButton(
	child: Text("Go to screen a"),
	style: ElevatedButton.styleFrom(
	primary: blackButton ? Colors.black : Colors.white,
	),
	onPressed: () {

	},
),

Remote Config

Et voilà !

Remote Config

Pour confirmer tout ça, changeons la valeur côté Firebase :

Remote Config

Cependant, pour des raisons de performances, Firebase ne va pas venir récupérer tout le temps les mises à jour

Il est toutefois possible de configurer ça côté app Flutter !

await remoteConfig.setConfigSettings(RemoteConfigSettings(
  fetchTimeout: Duration(seconds: 10),
  minimumFetchInterval: Duration(hours: 1),
));

Remote Config

Pour le test, je vais mettre un temps d'interval très cours

Et voilà !!

Remote Config

Et la popup bloquante dans tout ça ?

Je vous laisse le faire vous-même !

Je corrigerai après :)

Petit indice, vous aurez besoin du package

Remote Config

Petit indice, vous aurez besoin du package

showGeneralDialog(
  context: context,
  barrierDismissible: false,
  pageBuilder: (context, animation1, animation2) {
    return Dialog(
      child: Container(
        height: 300,
        color: Colors.white,
        child: Center(
          child: Text(
            "ok",
            style: Theme.of(context).textTheme.button,
          ),
        ),
      ),
    );
  },
);

Pour afficher une dialog

Firebase Storage

Firebase Storage

Qu'est-ce que Firebase Storage ?

C'est le service de "Bucket" de documents de Firebase

On trouve des "Buckets" chez AWS, Google, Azure, etc. Il s'agit de services cloud de stockage de photos, vidéos et tout type de documents

Firebase Storage

L'intérêt ?

Stocker et distribuer des documents est une problématique qui peut devenir très compliquée sur sa propre API

Utiliser ce genre de services permet de s'assurer de la scalabilité des services, de la sécurité des transferts, de leur performance, etc.

Firebase Storage

OK, c'est parti :

firebase_storage: "^8.1.1"

Côté plateforme Firebase, il faut également activer notre Bucket Firebase Storage

Firebase Storage

Pour récupérer l'instance de Storage, ça se passe un peu comme pour tous les autres services : 

final FirebaseStorage firebaseStorage = FirebaseStorage.instance;

Firebase Storage

Ensuite, on peut très facilement récupérer les références de nos fichiers comme ceci, un peu à la manière de Firestore :

final Reference reference = firebaseStorage.ref("/notes.txt");

Et, comme Firestore, chercher plus profondément :

final Reference reference = firebaseStorage.ref("/images/profilePicture.png");

Firebase Storage

Ou encore :

firebaseStorage
      .ref()
      .child('images')
      .child('profilePicture.png');

Firebase Storage

On a également moyen de récupérer la liste des documents dans un dossier :

final ListResult result = await firebaseStorage.ref().listAll();

result.items.forEach((Reference ref) {
  print('Found file: $ref');
});

(Bon, après, ça prend du temps ça !)

Firebase Storage

Côté mobile, outre les documents, ce qui nous intéresse 99% du temps c'est : stocker une image ou afficher une image depuis une URL

En vrai, on ne "télécharge" jamais l'image nous même, on donne une URL fournie par l'API à un composant qui est capable d'afficher l'image correspondante avec gestion du placeholder et de l'erreur le cas échéant

Firebase Storage

Déjà, côté URL, on a cette instruction qui nous permet d'avoir l'URL associée pour un document sur Storage :

  final String url = await firebaseStorage
  .ref('users/123/avatar.jpg')
  .getDownloadURL();

Firebase Storage

Ok, maintenant, comment upload une image ?

  final String url = await firebaseStorage
  .ref('users/123/avatar.jpg')
  .getDownloadURL();

Firebase Storage

Ok, maintenant, comment upload une image ?

Pour ce faire, il nous faut récupérer un chemin absolu vers l'image sur le téléphone, pour ce faire on peut par exemple utiliser le package

Ensuite, ça donne quelque chose comme ça :

final Directory appDocDir = await getApplicationDocumentsDirectory();
  final String filePath = '${appDocDir.absolute}/file-to-upload.png';
  final File file = File(filePath);

  try {
    await firebaseStorage.ref('uploads/file-to-upload.png').putFile(file);
  } on FirebaseException catch (e) {
    // e.g, e.code == 'canceled'
  }

Firebase Storage

La plupart du temps, on va récupérer l'image depuis un image picker

Codons un exemple avec :

(Côté iOS, il ne faudra pas oublier de renseigner les bonnes infos dans votre Plist concernant les autorisations)

Firebase Storage

Installons le package :

image_picker: 0.8.0+3

Firebase Storage

Côté code, on peut commence à s'amuser avec cette petite méthode :

Future _pickImage() async {
  final pickedFile = await ImagePicker().getImage(source: ImageSource.camera);

  if (pickedFile != null) {
    final File file = File(pickedFile.path);
    final UploadTask uploadTask = FirebaseStorage.instance.ref("imgages").putFile(file);
    uploadTask.snapshotEvents.listen((TaskSnapshot snapshot) {
      print("Task state: ${snapshot.state}");
      print("Progress: ${(snapshot.bytesTransferred / snapshot.totalBytes) * 100} %");
    }, onError: (e) {
    print(uploadTask.snapshot);

    if (e.code == 'permission-denied') {
      print('User does not have permission to upload to this reference.');
    }
  });
  } else {
    print('No image selected.');
  }
}

Firebase Storage

Attention, il est possible que vous vous preniez une 403

En effet, par défaut, Firebase Storage limite l'écriture sur Storage aux utilisateurs connectés (via Firebase Auth)

Ca, en théorie, c'est plutôt cool !

Mais pour notre exemple tout bête, on va retirer cette règle (pensez cependant à la remettre plus tard !)

Firebase Storage

Pour ce faire, on passe de ça :

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow read, write: if request.auth != null;
    }
  }
}

à ça :

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow read, write;
    }
  }
}

Firebase Storage

C'est bon, vous pouvez tester l'upload, normalement tout est OK :)

Dynamic Links

Dynamic Links

Un dynamic link, qu'est-ce que c'est ?

Est-ce qu'il vous est déjà arrivé de recevoir un lien qu'un ami vous a partagé, de cliquer dessus, et d'être automatiquement redirigé vers l'app associée sur votre téléphone ?

C'est ça, un Dynamic Link !

Dynamic Links

Un Dynamic Link est un lien qui va pouvoir être interprété différemment (de manière dynamique) selon le contexte dans lequel il est ouvert

L'utilisateur a l'application ? Alors ouvrons l'application ! Sinon, envoyons le sur le Store pour qu'il la télécharge. Il est sur le web ? Envoyons le sur l'app web correspondante, ou sur la fiche de store, etc.

Dynamic Links

Vous avez peut-être déjà entendu parler de DeepLink (si c'est pas le cas, ça arrivera, promis)

Mais alors, pour vous, quelle est la différence entre un DeepLink et un Dynamic Link ?

Dynamic Links

Prenons l'example d'un lien qui ouvre votre application, déjà c'est cool

Mais imaginons maintenant que ce soit une application de News et que le lien ouvre le bon article correspondant dans l'application !

Ça, c'est un DeepLink !

Dynamic Links

On appelle DeepLink les liens qui permettent de naviguer en "profondeur" dans l'application

Chaque OS a son système de DeepLink :

  • iOS : Universal Link
  • Android : App Link

Dynamic Links

En gros, un Dynamic Link, c'est un lien Firebase qui encapsule un DeepLink, comme une boîte

Firebase se charge, via DynamicLink de livrer le lien au bon destinataire (app, Store, site, etc.) puis va ensuite fournir le DeepLink à l'OS qui va l'interpreter

Dynamic Links

Comment ça fonctionne ?

En fait, c'est Firebase qui va héberger votre lien

Lorsque vous cliquez, Firebase va pouvoir décider où vous envoyer puis va fournir le DeepLink

Du coup... il va falloir avoir un nom de domaine pour pouvoir héberger nos liens !

Dynamic Links

Pour ce faire, direction Hosting de Firebase, un nouveau service :)

On esquive toutes les configs en faisant "suivant"

Dynamic Links

Firebase vous fournit des noms de domaines gratuits par défaut mais vous pourrez évidemment ajouter le votre !

Dynamic Links

Ok, on peut mettre en place les dynamic links

Dynamic Links

Soit vous avez votre propre domaine (déclaré sur Hosting) soit vous en prenez un offert par Google

Dynamic Links

Nous allons créer une logique de DeepLinks en imaginant qu'ils ouvrent le détail d'un élément de notre application

Par exemple, si c'était une application de Média, nous pourrions imaginer ce genre de DeepLink :

monlink/article/[id]

Dynamic Links

Cliquez sur "nouveau lien dynamique", et créer un lien

Dynamic Links

Dynamic Links

Dynamic Links

Maintenant, nous allons voir comment recevoir un Dynamic Link côté app Flutter 

(Nous ferons tout sur Android car il faut un compte Déveloper pour Apple, mais vous trouverez toutes les infos dans la doc Firebase pour les configurations Apple)

Dynamic Links

On commence par installer le package

flutter pub add firebase_dynamic_links

Dynamic Links

Côté Android, on ajoute un Intent-Filter dans le Manifest pour autoriser nos Dynamic Links à ouvrir l'application

<intent-filter>
    <action android:name="android.intent.action.VIEW"/>
    <category android:name="android.intent.category.DEFAULT"/>
    <category android:name="android.intent.category.BROWSABLE"/>
    <data
        android:host="testflutter3.com"
        android:scheme="https"/>
</intent-filter>

Dynamic Links

Par définition, un Dynamic Link peut provoquer l'ouverture de l'app chez un utilisateur, il est donc important de récupérer un potentiel lien dans la méthode Main, pour être sûr de gérer celui-ci avant toute logique de l'app

Dynamic Links

Ce qui donne quelque chose comme :

final PendingDynamicLinkData? initialLink = await FirebaseDynamicLinks.instance.getInitialLink();
runApp(MyApp(initialLink));

C'est bon !

Dynamic Links

Imaginons maintenant un widget de détail d'article comme celui-ci :

import 'package:flutter/material.dart';

class ArticleDetail extends StatelessWidget {
  static String routeName = "/ArticleDetail";

  static Future<void> navigateTo(BuildContext context, int articleId) {
    return Navigator.of(context).pushNamed(routeName, arguments: articleId);
  }

  final int articleId;

  const ArticleDetail({
    Key? key,
    required this.articleId,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text("Détail de l'article $articleId"),
      ),
    );
  }
}

Dynamic Links

Imaginons une MaterialApp avec ce router :

return MaterialApp(
      onGenerateRoute: (RouteSettings routeSettings) {
        Widget screen = Container();

        switch (routeSettings.name) {
          case ArticleDetail.routeName:
            if (routeSettings.arguments != null && routeSettings.arguments is int) {
              screen = ArticleDetail(articleId: routeSettings.arguments as int);
            }
            break;
        }

        return MaterialPageRoute(
          builder: (context) {
            return screen;
          },
        );
      },
      home: Home(
        pendingDynamicLinkData: pendingDynamicLinkData,
      ),
    );

Dynamic Links

Imaginons une MaterialApp avec ce router :

return MaterialApp(
	onGenerateRoute: (RouteSettings routeSettings) {
		Widget screen = Container();

		switch (routeSettings.name) {
			case ArticleDetail.routeName:
				if (routeSettings.arguments != null && routeSettings.arguments is int) {
				screen = ArticleDetail(articleId: routeSettings.arguments as int);
				}
			break;
		}

		return MaterialPageRoute(
			builder: (context) {
				return screen;
			},
		);
	},
	home: Home(
		pendingDynamicLinkData: pendingDynamicLinkData,
	),
);

Dynamic Links

On va maintenant imaginer que notre Home est chargée de gérer les Dynamic/Deep links comme ceci :

void _initDynamicLinks() async {
    if (widget.pendingDynamicLinkData != null) {
      _handleDeepLink(widget.pendingDynamicLinkData!);
    }
  }

  void _handleDeepLink(PendingDynamicLinkData dynamicLinkData) {
    final Uri link = dynamicLinkData.link;
    if (link.queryParameters.containsKey("articleId")) {
      try {
        final int articleId = int.parse(link.queryParameters["articleId"] as String);
        ArticleDetail.navigateTo(context, articleId);
      } catch (error) {
        print("Unable to parse article ID into INT");
      }
    }
  }

Dynamic Links

Enfin, nous pouvons également géré le fait de recevoir un Dynamic Link lorsque l'application est déjà ouverte, en Background donc :

void _initDynamicLinks() async {
    if (widget.pendingDynamicLinkData != null) {
      _handleDeepLink(widget.pendingDynamicLinkData!);
    }

    FirebaseDynamicLinks.instance.onLink.listen(_handleDeepLink).onError((error) {
      /// Handle Error on opening Dynamic Link
    });
  }

Dynamic Links

Et voilà !

Vous pouvez tester en vous envoyer le DynamicLink sur Slack ou autre, cliquez dessus sur votre téléphone et vous verrez toute la logique se mettre en place :)

Dynamic Links

Il ne nous reste plus qu'une chose à gérer : comment dynamiquement créer des liens ?

En effet, créer des liens via la plateforme Firebase est une chose, mais l'énorme utilité de ce système est que les utilisateurs puissent dynamiquement s'envoyer entre eux des liens (exemple : je veux partager avec mes amis le lieu que j'ai trouvé sur AirBnB pour nos vacances)

Dynamic Links

Toujours dans notre exemple d'articles, on va imaginer un bouton qui nous permets de "partager" un article et qui va donc générer le lien

Une fois le lien créé, il nous faudra évidemment pouvoir le partager, nous aurons donc également besoin d'un package comme celui-ci par exemple qui est très efficace :

Dynamic Links

Voici l'API disponible côté Flutter pour créer un lien et comment nous pourrions l'utiliser :

void _share() async {
    final dynamicLinkParams = DynamicLinkParameters(
      link: Uri.parse("https://test_flutter3.com/detail/?articleId=42"),
      uriPrefix: "https://dynamicccc.page.link",
      androidParameters: const AndroidParameters(
        packageName: "com.example.dynamic_test",
        minimumVersion: 0,
      ),
      iosParameters: const IOSParameters(
        bundleId: "com.example.dynamic_test",
        appStoreId: "123456789",
        minimumVersion: "1.0.1",
      ),
      googleAnalyticsParameters: const GoogleAnalyticsParameters(
        source: "twitter",
        medium: "social",
        campaign: "example-promo",
      ),
      socialMetaTagParameters: SocialMetaTagParameters(
        title: "Article 42",
        description: "Découvrez cet article super intéressant : ",
        imageUrl: Uri.parse(
            "https://media.istockphoto.com/photos/spongebob-squarepants-picture-id458134167?k=20&m=458134167&s=612x612&w=0&h=0ec6DQkmc-R8NgpE4TMZaapaeTVaqTvAtrmhZd2IxgE="),
      ),
    );

    final ShortDynamicLink shortDynamicLink =
        await FirebaseDynamicLinks.instance.buildShortLink(dynamicLinkParams);
    final Uri shortUrl = shortDynamicLink.shortUrl;
    try {
      Share.share(
        shortUrl.toString(),
        subject: "Voici un article super intéressant",
      );
    } catch (error) {
      print("Error sharing dynamic link : $error");
    }
  }

Firebase

By Ecalle Thomas

Firebase

Cours de 15h sur les outils Firebase orientés développement d'application mobile

  • 586