Petit point sur le choix de Flutter comme technologie
Qu'est-ce que c'est ?
Voilà... je vous ai pas menti sur le côté "mini" de l'historique.
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).
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
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)
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 :)
Pour commencer à jouer avec Firebase, RDV sur la :
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
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
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 :
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
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 ?
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
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 !
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 :
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
Dernier "détail", et pas des moindre, avant de créer notre premier projet :
Un projet Firebase, c'est un projet Google Cloud
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
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
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
Il y a également des concurrents Open Source !
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
OK.
Maintenant, on peut créer notre premier projet Firebase :
On donne d'abord le nom de notre projet
Observez l'identifiant généré. C'est lui qui doit être unique
Ajouter (ou non) Google Analytics à notre projet
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
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
Sinon, vous pouvez en créer un
Et voilà !
Sur le panneau de gauche, vous trouverez l'ensemble des services proposés par Firebase
Sélection du projet
Lier une application (iOS, Android ou web) au projet
Paramètre généraux du projet
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
MAIS
Une très grosse partie peut être entièrement gratuite
En fait, vous avez 2 plans possible :
Beaucoup de services sont gratuits quoi qu'il arrive :
Pour les autres services, ça dépend de votre mode :
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
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
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 !
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)
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
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
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)
Si vous voulez tester un peu tout ça, Google propose un programme de 300$ de crédits disponible sur 90 jours
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 ?
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 upgradeCommençons donc par créer notre application, avec Android Studio (ou VSCode) ou directement en ligne de commande
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
Attention, pour mettre en place le null-safety, on va upgrade la version de Dart requise en passant de :
environment:
sdk: ">=2.7.0 <3.0.0"à :
environment:
sdk: ">=2.12.0 <3.0.0"Commençons par Android :
Ici, seul le package Android est obligatoire. Vous le trouverez dans le build.gradle (app) de votre application
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
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 ../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
(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.
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
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
(Petit aparté sur la signature auto-gérée par le PlayStore)
Ok, on peut continuer !
Cette étape est la plus importante !
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
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
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
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
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 :
Ensuite, ils ne nous reste plus qu'à déclarer et appliquer le plugin google-services dans notre Gradle
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 64KC'est fini pour Android !
Vous pouvez passer les étapes suivantes sur la doc et retourner ensuite sur votre projet Firebase dans votre console
Ici, c'est pareil que sur Android, la seule information nécessaire est le Bundle Id de votre application !
On va devoir ensuite télécharger le fichier de confs pour 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 !
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
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
Le 11 mai 2022, Flutter 3 a été annoncé à la Google I/O !
Au programme :
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 :
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.
Pour cela il faudra au préalable télécharger l'utilitaire Firebase en ligne de commandes :
S'y connecter via la commande
firebase loginEnsuite, il n'y a plus qu'à suivre les étapes, et hop, tout est fait pour vous au niveau de la gestion des configurations :)
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());
}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
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.
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 :
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 :)
(j'avais pas trop d'idée de titre j'avoue)
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
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
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"Quelle est la différence entre Legacy et Null safety ?
Tous les packages Firebase ne sont pas encore passés en mode null-safety !
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
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
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();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();
},
);
}
}Ou alors... beaucoup plus simple :
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(
AnalyticsManager(
analytics: FirebaseAnalytics(),
child: MaterialApp(
home: Home(),
),
),
);
}Pour commencer, on ajoute la dépendance
dependencies:
flutter:
sdk: flutter
firebase_core: version.recente
firebase_analytics: version.recenteAttention, sur Android il va sûrement falloir mettre le minSDK à 19 et sur iOS, dans le PodFile, la platform à 10
Et voilà !
Rien qu'en mettant la dépendance, beaucoup d'évènements seront loggés sur votre Dashboard
Firebase Analytics va vous permettre de logger des évènements ainsi que des user properties
Les deux portent plutôt bien leurs noms :
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.)
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 !
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é !
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
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',
);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
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 :
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)
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");
},
),
),
),
);
}
}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)
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
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.Une fois que c'est fait, direction la Debug View sur votre projet Firebase :
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 ! :)
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
Petite correction avec l'observer de Navigation disponible sur le dépôt GitHub du cours :
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 :
Il existe en fait 2 services distincts de BDD qui sont :
Les 2 services sont des BDD NO-SQL avec les caractéristiques citées précédemment
Petit aparté sur le NO-SQL
Pour vous, c'est quoi ?
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
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)
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 !
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"
BREF
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 :
A savoir aussi que les données ne sont pas du tout stockées sous le même format NO-SQL :
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. ...
Nous étudierons dans ce cours Firestore !
Mais n'hésitez pas à vous renseigner sur Realtime Database
Commençons par activer le service associé dans notre console Firebase :
Choisissez ensuite le mode test pour commencer :
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 !
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"
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
Tout est OK niveau console Firebase !
Ajoutons maintenant la dépendance côté Flutter pour pouvoir utiliser Firestore :
cloud_firestore: "2.2.1"Et normalement.. ça devrait être bon !
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
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");
}
}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 ;)
Problème de lenteur dans le XCode Build côté iOS ?
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.
Si on reprend l'exemple que vous avez développé, ça donne ça :
Notez que Firestore crée des identifiants par défauts à vos documents mais que vous pourriez potentiellement en mettre un vous-même
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
Pour connaître tous les types possible pour un champs d'un Document, je vous laisse voir ça
É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');É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
É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"});É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");
}
}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"});Lire la BDD
Lorsque vous demandez à lire des données depuis Firestore, vous avez la possibilité de le faire de 2 manières :
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é
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
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
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();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();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
Lire la BDD - queries
On peut mettre
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);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");
}
}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
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é
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"));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.
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éé
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
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.
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.
N'importe quelle application mise en prod DOIT avoir une solution de reporting de crashs comme Crashlytics, l'inverse serait vraiment inconscient
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
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
Android :
dans le fichier android/app/build.gradle, ajoutez :
apply plugin: 'com.google.firebase.crashlytics'Ancienne façon
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
iOS :
${PODS_ROOT}/FirebaseCrashlytics/runAncienne façon
Enfin, il nous reste à activer Crashlytics sur la console Firebase
Attention, il faut l'activer pour Android ET pour iOS
Ancienne façon
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
Nouveau
Désormais, tout est plus simple :
Attention à bien lancer le
flutterfire configurequi va venir changer les choses pour nous dans le build.gradle etc.
Nous allons donc faire ça mais, avant, nous allons écrire le code nécessaire pour la remontée des bugs côté Flutter
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());
}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);
}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);
}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);
}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();Ç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 !");
}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à :
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'
);
}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
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
Ensuite, il est important de comprendre que les applications ont 3 états différents possibles :
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}');Le AuthorizationStatus peut avoir alors différentes valeurs :
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 :
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}');
}
}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
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
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}');
}
});Vous pouvez de nouveau tester votre notif en gardant votre application en Foreground, et vous arriverez à l'afficher dans vos logs
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,
);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)
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)
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,
);
}Puis, lors de la réception d'une notification en Foreground, nous allons utiliser le package pour :
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,
),
),
);
}
}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
Nous voilà déjà bien avancés, mais on va voir encore quelques trucs :
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}");
}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
Grâce à ce Token, nous pourrons faire 2 choses intéressantes :
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
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
Ça se passe ici :
Ça se passe ici :
Renseignez votre Token, et c'est bon !
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
Dans un premier temps : OUI, c'est très "simple" et c'est quelque chose qu'on pourrait faire faire par notre API
Mais :
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
Qu'est-ce qu'on met dans un Remote Config ?
Allez, c'est parti, on le met en place :
firebase_remote_config: "^0.10.0+1"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
Mettons en place 2 configurations :
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
Bien sûr, il faut ensuite publier les changements !
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");ElevatedButton(
child: Text("Go to screen a"),
style: ElevatedButton.styleFrom(
primary: blackButton ? Colors.black : Colors.white,
),
onPressed: () {
},
),Et voilà !
Pour confirmer tout ça, changeons la valeur côté Firebase :
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),
));Pour le test, je vais mettre un temps d'interval très cours
Et voilà !!
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
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
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
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.
OK, c'est parti :
firebase_storage: "^8.1.1"Côté plateforme Firebase, il faut également activer notre Bucket 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;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");Ou encore :
firebaseStorage
.ref()
.child('images')
.child('profilePicture.png');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 !)
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
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();
Ok, maintenant, comment upload une image ?
final String url = await firebaseStorage
.ref('users/123/avatar.jpg')
.getDownloadURL();
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'
}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)
Installons le package :
image_picker: 0.8.0+3Cô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.');
}
}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 !)
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;
}
}
}C'est bon, vous pouvez tester l'upload, normalement tout est OK :)
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 !
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.
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 ?
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 !
On appelle DeepLink les liens qui permettent de naviguer en "profondeur" dans l'application
Chaque OS a son système de DeepLink :
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
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 !
Pour ce faire, direction Hosting de Firebase, un nouveau service :)
On esquive toutes les configs en faisant "suivant"
Firebase vous fournit des noms de domaines gratuits par défaut mais vous pourrez évidemment ajouter le votre !
Ok, on peut mettre en place les dynamic links
Soit vous avez votre propre domaine (déclaré sur Hosting) soit vous en prenez un offert par Google
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]
Cliquez sur "nouveau lien dynamique", et créer un lien
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)
On commence par installer le package
flutter pub add firebase_dynamic_linksCô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>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
Ce qui donne quelque chose comme :
final PendingDynamicLinkData? initialLink = await FirebaseDynamicLinks.instance.getInitialLink();
runApp(MyApp(initialLink));C'est bon !
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"),
),
);
}
}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,
),
);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,
),
);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");
}
}
}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
});
}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 :)
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)
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 :
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");
}
}