Apprenez à connaître Firebase pour Flutter

1. Avant de commencer

Dans cet atelier de programmation, vous apprendrez certaines des bases de Firebase pour créer des applications mobiles Flutter pour Android et iOS.

Conditions préalables

Cet atelier de programmation suppose que vous connaissez Flutter et que vous avez installé le SDK Flutter et un éditeur .

Ce que vous allez créer

Dans cet atelier de programmation, vous allez créer une application de chat RSVP d'événement et de livre d'or sur Android, iOS, le Web et macOS à l'aide de Flutter. Vous authentifierez les utilisateurs avec Firebase Authentication et synchroniserez les données à l'aide de Cloud Firestore.

Ce dont vous aurez besoin

Vous pouvez exécuter cet atelier de programmation à l'aide de l'un des appareils suivants :

  • Un appareil physique (Android ou iOS) connecté à votre ordinateur et défini en mode développeur.
  • Le simulateur iOS. (Nécessite l'installation des outils Xcode .)
  • L'émulateur Android. (Nécessite une configuration dans Android Studio .)

En plus de ce qui précède, vous aurez également besoin de :

  • Un navigateur de votre choix, tel que Chrome.
  • Un IDE ou un éditeur de texte de votre choix, comme Android Studio ou VS Code configuré avec les plugins Dart et Flutter.
  • La dernière version stable de Flutter (ou beta si vous aimez vivre à la limite).
  • Un compte Google, comme un compte gmail, pour créer et gérer votre projet Firebase.
  • L' outil de ligne de commande firebase , connecté à votre compte gmail.
  • L'exemple de code du codelab. Voir l'étape suivante pour savoir comment obtenir le code.

2. Obtenez l'exemple de code

Commençons par télécharger la version initiale de notre projet depuis GitHub.

Clonez le dépôt GitHub à partir de la ligne de commande :

git clone https://github.com/flutter/codelabs.git flutter-codelabs

Alternativement, si vous avez installé l'outil cli de GitHub :

gh repo clone flutter/codelabs flutter-codelabs

L'exemple de code doit être cloné dans le répertoire flutter-codelabs , qui contient le code d'une collection de codelabs. Le code de ce laboratoire de programmation se trouve dans flutter-codelabs/firebase-get-to-know-flutter .

La structure de répertoire sous flutter-codelabs/firebase-get-to-know-flutter est une série d'instantanés de l'endroit où vous devriez être à la fin de chaque étape nommée. Il s'agit de l'étape 2, donc localiser les fichiers correspondants est aussi simple que :

cd flutter-codelabs/firebase-get-to-know-flutter/step_02

Si vous souhaitez avancer ou voir à quoi quelque chose devrait ressembler après une étape, regardez dans le répertoire nommé d'après l'étape qui vous intéresse.

Importer l'application de démarrage

Ouvrez ou importez le flutter-codelabs/firebase-get-to-know-flutter/step_02 dans votre IDE préféré. Ce répertoire contient le code de démarrage du laboratoire de programmation qui consiste en une application de rencontre Flutter pas encore fonctionnelle.

Localisez les fichiers sur lesquels travailler

Le code de cette application est réparti sur plusieurs répertoires. Cette répartition des fonctionnalités est conçue pour faciliter le travail, en regroupant le code par fonctionnalité.

Localisez les fichiers suivants dans le projet :

  • lib/main.dart : Ce fichier contient le point d'entrée principal et le widget de l'application.
  • lib/src/widgets.dart : Ce fichier contient une poignée de widgets pour aider à standardiser le style de l'application. Ceux-ci sont utilisés pour composer l'écran de l'application de démarrage.
  • lib/src/authentication.dart : ce fichier contient une implémentation partielle de FirebaseUI Auth avec un ensemble de widgets pour créer une expérience utilisateur de connexion pour l'authentification basée sur les e-mails Firebase. Ces widgets pour le flux d'authentification ne sont pas encore utilisés dans l'application de démarrage, mais vous les connecterez bientôt.

Vous ajouterez des fichiers supplémentaires si nécessaire pour développer le reste de l'application.

Examen du fichier lib/main.dart

Cette application tire parti du package google_fonts pour nous permettre de faire de Roboto la police par défaut dans toute l'application. Un exercice pour le lecteur motivé consiste à explorer fonts.google.com et à utiliser les polices que vous y découvrez dans différentes parties de l'application.

Vous utilisez les widgets d'assistance de lib/src/widgets.dart sous la forme de Header , Paragraph et IconAndDetail . Ces widgets réduisent l'encombrement de la mise en page décrite dans HomePage en éliminant le code dupliqué. Cela a l'avantage supplémentaire de permettre une apparence et une sensation cohérentes.

Voici à quoi ressemble votre application sur Android, iOS, le Web et macOS :

Aperçu de l'application

3. Créer et configurer un projet Firebase

L'affichage des informations sur l'événement est idéal pour vos invités, mais le simple fait d'afficher les événements n'est très utile pour personne. Ajoutons quelques fonctionnalités dynamiques à cette application. Pour cela, vous devrez connecter Firebase à votre application. Pour démarrer avec Firebase, vous devez créer et configurer un projet Firebase.

Créer un projet Firebase

  1. Connectez-vous à Firebase .
  2. Dans la console Firebase, cliquez sur Ajouter un projet (ou Créer un projet ) et nommez votre projet Firebase Firebase-Flutter-Codelab .

4395e4e67c08043a.png

  1. Cliquez sur les options de création de projet. Acceptez les conditions de Firebase si vous y êtes invité. Ignorez la configuration de Google Analytics, car vous n'utiliserez pas Analytics pour cette application.

b7138cde5f2c7b61.png

Pour en savoir plus sur les projets Firebase, consultez Comprendre les projets Firebase .

L'application que vous créez utilise plusieurs produits Firebase disponibles pour les applications Web :

  • Firebase Authentication pour permettre à vos utilisateurs de se connecter à votre application.
  • Cloud Firestore pour enregistrer des données structurées sur le cloud et recevoir une notification instantanée lorsque les données changent.
  • Règles de sécurité Firebase pour sécuriser votre base de données.

Certains de ces produits nécessitent une configuration spéciale ou doivent être activés à l'aide de la console Firebase.

Activer la connexion par e-mail pour l'authentification Firebase

Pour autoriser les utilisateurs à se connecter à l'application Web, vous utiliserez la méthode de connexion par e-mail/mot de passe pour cet atelier de programmation :

  1. Dans la console Firebase, développez le menu Build dans le panneau de gauche.
  2. Cliquez sur Authentification , puis cliquez sur le bouton Commencer , puis sur l'onglet Méthode de connexion (ou cliquez ici pour accéder directement à l'onglet Méthode de connexion).
  3. Cliquez sur E-mail/Mot de passe dans la liste Fournisseurs de connexion, réglez le commutateur Activer sur la position Activé, puis cliquez sur Enregistrer . 58e3e3e23c2f16a4.png

Activer Cloud Firestore

L'application Web utilise Cloud Firestore pour enregistrer les messages de chat et recevoir de nouveaux messages de chat.

Activez Cloud Firestore :

  1. Dans la section Build de la console Firebase, cliquez sur Cloud Firestore .
  2. Cliquez sur Créer une base de données . 99e8429832d23fa3.png
  1. Sélectionnez l'option Démarrer en mode test . Lisez l'avertissement sur les règles de sécurité. Le mode test garantit que vous pouvez écrire librement dans la base de données pendant le développement. Cliquez sur Suivant . 6be00e26c72ea032.png
  1. Sélectionnez l'emplacement de votre base de données (vous pouvez simplement utiliser la valeur par défaut). Notez que cet emplacement ne peut pas être modifié ultérieurement. 278656eefcfb0216.png
  2. Cliquez sur Activer .

4. Configuration Firebase

Pour utiliser Firebase avec Flutter, vous devez suivre un processus pour configurer le projet Flutter afin d'utiliser correctement les bibliothèques FlutterFire :

  • Ajoutez les dépendances FlutterFire à votre projet
  • Enregistrez la plate-forme souhaitée sur le projet Firebase
  • Téléchargez le fichier de configuration spécifique à la plate-forme et ajoutez-le au code.

Dans le répertoire de niveau supérieur de votre application Flutter, il existe des sous-répertoires appelés android , ios , macos et web . Ces répertoires contiennent les fichiers de configuration spécifiques à la plate-forme pour iOS et Android, respectivement.

Configurer les dépendances

Vous devez ajouter les bibliothèques FlutterFire pour les deux produits Firebase que vous utilisez dans cette application - Firebase Auth et Cloud Firestore. Exécutez les trois commandes suivantes pour ajouter les dépendances.

$ flutter pub add firebase_core 
Resolving dependencies...
+ firebase_core 1.10.5
+ firebase_core_platform_interface 4.2.2
+ firebase_core_web 1.5.2
+ flutter_web_plugins 0.0.0 from sdk flutter
+ js 0.6.3
  test_api 0.4.3 (0.4.8 available)
Changed 5 dependencies!

Le firebase_core est le code commun requis pour tous les plugins Firebase Flutter.

$ flutter pub add firebase_auth
Resolving dependencies...
+ firebase_auth 3.3.3
+ firebase_auth_platform_interface 6.1.8
+ firebase_auth_web 3.3.4
+ intl 0.17.0
  test_api 0.4.3 (0.4.8 available)
Changed 4 dependencies!

firebase_auth permet l'intégration avec la capacité d'authentification de Firebase.

$ flutter pub add cloud_firestore
Resolving dependencies...
+ cloud_firestore 3.1.4
+ cloud_firestore_platform_interface 5.4.9
+ cloud_firestore_web 2.6.4
  test_api 0.4.3 (0.4.8 available)
Changed 3 dependencies!

Le cloud_firestore permet d'accéder au stockage de données Cloud Firestore.

$ flutter pub add provider
Resolving dependencies...
+ nested 1.0.0
+ provider 6.0.1
  test_api 0.4.3 (0.4.8 available)
Changed 2 dependencies!

Bien que vous ayez ajouté les packages requis, vous devez également configurer les projets iOS, Android, macOS et Web Runner pour utiliser correctement Firebase. Vous utilisez également le package de provider qui permettra de séparer la logique métier de la logique d'affichage.

Installation flutterfire

La CLI FlutterFire dépend de la CLI Firebase sous-jacente. Si vous ne l'avez pas déjà fait, assurez-vous que la CLI Firebase est installée sur votre machine.

Ensuite, installez la CLI FlutterFire en exécutant la commande suivante :

$ dart pub global activate flutterfire_cli

Une fois installée, la commande flutterfire sera disponible dans le monde entier.

Configuration de vos applications

La CLI extrait les informations de votre projet Firebase et des applications de projet sélectionnées pour générer toute la configuration d'une plate-forme spécifique.

À la racine de votre application, exécutez la commande configure :

$ flutterfire configure

La commande de configuration vous guidera à travers un certain nombre de processus :

  1. Sélection d'un projet Firebase (basé sur le fichier .firebaserc ou depuis la console Firebase).
  2. Indiquez les plates-formes (par exemple, Android, iOS, macOS et Web) pour lesquelles vous souhaitez configurer.
  3. Identifiez les applications Firebase pour les plates-formes choisies qui doivent être utilisées pour extraire la configuration. Par défaut, la CLI tentera de faire correspondre automatiquement les applications Firebase en fonction de la configuration actuelle de votre projet.
  4. Générez un fichier firebase_options.dart dans votre projet.

Configurer macOS

Flutter sur macOS crée des applications entièrement en bac à sable. Comme cette application s'intègre en utilisant le réseau pour communiquer avec les serveurs Firebase, vous devrez configurer votre application avec les privilèges du client réseau.

macos/Runner/DebugProfile.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.app-sandbox</key>
	<true/>
	<key>com.apple.security.cs.allow-jit</key>
	<true/>
	<key>com.apple.security.network.server</key>
	<true/>
  <!-- Add the following two lines -->
	<key>com.apple.security.network.client</key>
	<true/>
</dict>
</plist>

macos/Runner/Release.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.app-sandbox</key>
	<true/>
  <!-- Add the following two lines -->
	<key>com.apple.security.network.client</key>
	<true/>
</dict>
</plist>

Voir Autorisations et App Sandbox pour plus de détails.

5. Ajouter une connexion utilisateur (RSVP)

Maintenant que vous avez ajouté Firebase à l'application, vous pouvez configurer un bouton RSVP qui enregistre les personnes utilisant Firebase Authentication . Pour Android natif, iOS natif et Web, il existe des packages FirebaseUI Auth prédéfinis, mais pour Flutter, vous devrez créer cette capacité.

Le projet que vous avez récupéré à l'étape 2 comprenait un ensemble de widgets qui implémente l'interface utilisateur pour la majeure partie du flux d'authentification. Vous implémenterez la logique métier pour intégrer Firebase Authentication dans l'application.

Logique métier avec fournisseur

Vous allez utiliser le package de provider pour rendre un objet d'état d'application centralisé disponible dans l'arborescence des widgets Flutter de l'application. Pour commencer, modifiez les importations en haut de lib/main.dart :

lib/main.dart

import 'package:firebase_auth/firebase_auth.dart'; // new
import 'package:firebase_core/firebase_core.dart'; // new
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';           // new

import 'firebase_options.dart';                    // new
import 'src/authentication.dart';                  // new
import 'src/widgets.dart';

Les lignes import introduisent Firebase Core et Auth, extraient le package de provider que vous utilisez pour rendre l'objet d'état de l'application disponible via l'arborescence des widgets et incluent les widgets d'authentification de lib/src .

Cet objet d'état d'application, ApplicationState , a deux responsabilités principales pour cette étape, mais acquerra des responsabilités supplémentaires au fur et à mesure que vous ajouterez des fonctionnalités à l'application dans les étapes ultérieures. La première responsabilité est d'initialiser la bibliothèque Firebase avec un appel à Firebase.initializeApp() , puis il y a la gestion du flux d'autorisation. Ajoutez la classe suivante à la fin de lib/main.dart :

lib/main.dart

class ApplicationState extends ChangeNotifier {
  ApplicationState() {
    init();
  }

  Future<void> init() async {
    await Firebase.initializeApp(
      options: DefaultFirebaseOptions.currentPlatform,
    );

    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loginState = ApplicationLoginState.loggedIn;
      } else {
        _loginState = ApplicationLoginState.loggedOut;
      }
      notifyListeners();
    });
  }

  ApplicationLoginState _loginState = ApplicationLoginState.loggedOut;
  ApplicationLoginState get loginState => _loginState;

  String? _email;
  String? get email => _email;

  void startLoginFlow() {
    _loginState = ApplicationLoginState.emailAddress;
    notifyListeners();
  }

  Future<void> verifyEmail(
    String email,
    void Function(FirebaseAuthException e) errorCallback,
  ) async {
    try {
      var methods =
          await FirebaseAuth.instance.fetchSignInMethodsForEmail(email);
      if (methods.contains('password')) {
        _loginState = ApplicationLoginState.password;
      } else {
        _loginState = ApplicationLoginState.register;
      }
      _email = email;
      notifyListeners();
    } on FirebaseAuthException catch (e) {
      errorCallback(e);
    }
  }

  Future<void> signInWithEmailAndPassword(
    String email,
    String password,
    void Function(FirebaseAuthException e) errorCallback,
  ) async {
    try {
      await FirebaseAuth.instance.signInWithEmailAndPassword(
        email: email,
        password: password,
      );
    } on FirebaseAuthException catch (e) {
      errorCallback(e);
    }
  }

  void cancelRegistration() {
    _loginState = ApplicationLoginState.emailAddress;
    notifyListeners();
  }

  Future<void> registerAccount(
      String email,
      String displayName,
      String password,
      void Function(FirebaseAuthException e) errorCallback) async {
    try {
      var credential = await FirebaseAuth.instance
          .createUserWithEmailAndPassword(email: email, password: password);
      await credential.user!.updateDisplayName(displayName);
    } on FirebaseAuthException catch (e) {
      errorCallback(e);
    }
  }

  void signOut() {
    FirebaseAuth.instance.signOut();
  }
}

Il convient de noter quelques points clés dans cette classe. L'utilisateur démarre non authentifié, l'application affiche un formulaire demandant l'adresse e-mail de l'utilisateur, selon que cette adresse e-mail est enregistrée, l'application demandera soit à l'utilisateur de s'inscrire, soit de demander son mot de passe, puis en supposant que tout fonctionne, l'utilisateur est authentifié.

Il convient de noter qu'il ne s'agit pas d'une implémentation complète du flux FirebaseUI Auth, car il ne gère pas le cas d'un utilisateur avec un compte existant qui rencontre des difficultés pour se connecter. L'implémentation de cette fonctionnalité supplémentaire est laissée à l'exercice. lecteur motivé.

Intégration du flux d'authentification

Maintenant que vous avez le début de l'état de l'application, il est temps de relier l'état de l'application à l'initialisation de l'application et d'ajouter le flux d'authentification dans HomePage . Mettez à jour le point d'entrée principal pour intégrer l'état de l'application via le package du provider :

lib/main.dart

void main() {
  // Modify from here
  runApp(
    ChangeNotifierProvider(
      create: (context) => ApplicationState(),
      builder: (context, _) => App(),
    ),
  );
  // to here.
}

La modification de la fonction main rend le package du fournisseur responsable de l'instanciation de l'objet d'état de l'application à l'aide du widget ChangeNotifierProvider . Vous utilisez cette classe de fournisseur spécifique car l'objet d'état de l'application étend ChangeNotifier et cela permet au package du provider de savoir quand réafficher les widgets dépendants. Enfin, intégrez l'état de l'application à l' Authentication en mettant à jour la méthode de build de HomePage :

lib/main.dart

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Firebase Meetup'),
      ),
      body: ListView(
        children: <Widget>[
          Image.asset('assets/codelab.png'),
          const SizedBox(height: 8),
          const IconAndDetail(Icons.calendar_today, 'October 30'),
          const IconAndDetail(Icons.location_city, 'San Francisco'),
          // Add from here
          Consumer<ApplicationState>(
            builder: (context, appState, _) => Authentication(
              email: appState.email,
              loginState: appState.loginState,
              startLoginFlow: appState.startLoginFlow,
              verifyEmail: appState.verifyEmail,
              signInWithEmailAndPassword: appState.signInWithEmailAndPassword,
              cancelRegistration: appState.cancelRegistration,
              registerAccount: appState.registerAccount,
              signOut: appState.signOut,
            ),
          ),
          // to here
          const Divider(
            height: 8,
            thickness: 1,
            indent: 8,
            endIndent: 8,
            color: Colors.grey,
          ),
          const Header("What we'll be doing"),
          const Paragraph(
            'Join us for a day full of Firebase Workshops and Pizza!',
          ),
        ],
      ),
    );
  }
}

Vous instanciez le widget Authentication et l'encapsulez dans un widget Consumer . Le widget Consommateur est la façon habituelle dont le package du provider peut être utilisé pour reconstruire une partie de l'arborescence lorsque l'état de l'application change. Le widget d' Authentication est l'interface utilisateur d'authentification que vous allez maintenant tester.

Test du flux d'authentification

cdf2d25e436bd48d.png

Voici le début du flux d'authentification, où l'utilisateur peut appuyer sur le bouton RSVP pour lancer le formulaire de courrier électronique.

2a2cd6d69d172369.png

Lors de la saisie de l'e-mail, le système confirme si l'utilisateur est déjà enregistré, auquel cas l'utilisateur est invité à entrer un mot de passe, sinon si l'utilisateur n'est pas enregistré, il passe par le formulaire d'inscription.

e5e65065dba36b54.png

Assurez-vous d'essayer de saisir un mot de passe court (moins de six caractères) pour vérifier le flux de traitement des erreurs. Si l'utilisateur est enregistré, il verra le mot de passe pour à la place.

fbb3ea35fb4f67a.png

Sur cette page, assurez-vous de saisir des mots de passe incorrects pour vérifier la gestion des erreurs sur cette page. Enfin, une fois que l'utilisateur est connecté, vous verrez l'expérience de connexion qui offre à l'utilisateur la possibilité de se déconnecter à nouveau.

4ed811a25b0cf816.png

Et avec cela, vous avez implémenté un flux d'authentification. Félicitations!

6. Écrivez des messages sur Cloud Firestore

Savoir que les utilisateurs arrivent, c'est bien, mais donnons aux invités autre chose à faire dans l'application. Et s'ils pouvaient laisser des messages dans un livre d'or ? Ils peuvent partager pourquoi ils sont ravis de venir ou qui ils espèrent rencontrer.

Pour stocker les messages de chat que les utilisateurs écrivent dans l'application, vous utiliserez Cloud Firestore .

Modèle de données

Cloud Firestore est une base de données NoSQL, et les données stockées dans la base de données sont divisées en collections, documents, champs et sous-collections. Vous stockerez chaque message du chat en tant que document dans une collection de niveau supérieur appelée guestbook d'or .

7c20dc8424bb1d84.png

Ajouter des messages à Firestore

Dans cette section, vous allez ajouter la fonctionnalité permettant aux utilisateurs d'écrire de nouveaux messages dans la base de données. Tout d'abord, vous ajoutez les éléments de l'interface utilisateur (champ de formulaire et bouton d'envoi), puis vous ajoutez le code qui relie ces éléments à la base de données.

Tout d'abord, ajoutez des importations pour le package cloud_firestore et dart:async .

lib/main.dart

import 'dart:async';                                    // new

import 'package:cloud_firestore/cloud_firestore.dart';  // new
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';

import 'firebase_options.dart';
import 'src/authentication.dart';
import 'src/widgets.dart';

Pour construire les éléments d'interface utilisateur d'un champ de message et d'un bouton d'envoi, ajoutez un nouveau widget avec état GuestBook au bas de lib/main.dart .

lib/main.dart

class GuestBook extends StatefulWidget {
  const GuestBook({required this.addMessage});
  final FutureOr<void> Function(String message) addMessage;

  @override
  _GuestBookState createState() => _GuestBookState();
}

class _GuestBookState extends State<GuestBook> {
  final _formKey = GlobalKey<FormState>(debugLabel: '_GuestBookState');
  final _controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Form(
        key: _formKey,
        child: Row(
          children: [
            Expanded(
              child: TextFormField(
                controller: _controller,
                decoration: const InputDecoration(
                  hintText: 'Leave a message',
                ),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Enter your message to continue';
                  }
                  return null;
                },
              ),
            ),
            const SizedBox(width: 8),
            StyledButton(
              onPressed: () async {
                if (_formKey.currentState!.validate()) {
                  await widget.addMessage(_controller.text);
                  _controller.clear();
                }
              },
              child: Row(
                children: const [
                  Icon(Icons.send),
                  SizedBox(width: 4),
                  Text('SEND'),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Il y a quelques points d'intérêt ici. Tout d'abord, vous instanciez un formulaire afin que vous puissiez valider que le message a réellement du contenu et montrer à l'utilisateur un message d'erreur s'il n'y en a pas. La façon de valider un formulaire consiste à accéder à l'état du formulaire derrière le formulaire, et pour cela, vous utilisez un GlobalKey . Pour plus d'informations sur les clés et comment les utiliser, veuillez consulter l' épisode Flutter Widgets 101 "Quand utiliser les clés" .

Notez également la façon dont les widgets sont disposés, vous avez un Row , avec un TextFormField et un StyledButton , qui contient lui-même un Row . Notez également que TextFormField est enveloppé dans un widget Expanded , ce qui force TextFormField à occuper tout espace supplémentaire dans la ligne. Pour mieux comprendre pourquoi cela est nécessaire, veuillez lire Comprendre les contraintes .

Maintenant que vous avez un widget qui permet à l'utilisateur de saisir du texte à ajouter au livre d'or, vous devez l'afficher à l'écran. Pour ce faire, modifiez le corps de HomePage pour ajouter les deux lignes suivantes au bas des enfants de ListView :

const Header("What we'll be doing"),
const Paragraph(
  'Join us for a day full of Firebase Workshops and Pizza!',
),
// Add the following two lines.
const Header('Discussion'),
GuestBook(addMessage: (message) => print(message)),

Bien que cela soit suffisant pour afficher le Widget, cela ne suffit pas pour faire quoi que ce soit d'utile. Vous mettrez à jour ce code sous peu pour le rendre fonctionnel.

Aperçu de l'application

Un utilisateur cliquant sur le bouton ENVOYER déclenchera l'extrait de code ci-dessous. Il ajoute le contenu du champ de saisie du message à la collection de livres d' guestbook de la base de données. Plus précisément, la méthode addMessageToGuestBook ajoute le contenu du message à un nouveau document (avec un ID généré automatiquement) à la collection de livres d' guestbook .

Notez que FirebaseAuth.instance.currentUser.uid est une référence à l'ID unique généré automatiquement que Firebase Authentication donne à tous les utilisateurs connectés.

Apportez une autre modification au fichier lib/main.dart . Ajoutez la méthode addMessageToGuestBook . Vous relierez l'interface utilisateur et cette fonctionnalité à l'étape suivante.

lib/main.dart

class ApplicationState extends ChangeNotifier {

  // Current content of ApplicationState elided ...

  // Add from here
  Future<DocumentReference> addMessageToGuestBook(String message) {
    if (_loginState != ApplicationLoginState.loggedIn) {
      throw Exception('Must be logged in');
    }

    return FirebaseFirestore.instance
        .collection('guestbook')
        .add(<String, dynamic>{
      'text': message,
      'timestamp': DateTime.now().millisecondsSinceEpoch,
      'name': FirebaseAuth.instance.currentUser!.displayName,
      'userId': FirebaseAuth.instance.currentUser!.uid,
    });
  }
  // To here
}

Câblage de l'interface utilisateur dans la base de données

Vous disposez d'une interface utilisateur dans laquelle l'utilisateur peut saisir le texte qu'il souhaite ajouter au livre d'or, et vous disposez du code pour ajouter l'entrée à Cloud Firestore. Maintenant, tout ce que vous avez à faire est de câbler les deux ensemble. Dans lib/main.dart apportez la modification suivante au widget HomePage .

lib/main.dart

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Firebase Meetup'),
      ),
      body: ListView(
        children: <Widget>[
          Image.asset('assets/codelab.png'),
          const SizedBox(height: 8),
          const IconAndDetail(Icons.calendar_today, 'October 30'),
          const IconAndDetail(Icons.location_city, 'San Francisco'),
          Consumer<ApplicationState>(
            builder: (context, appState, _) => Authentication(
              email: appState.email,
              loginState: appState.loginState,
              startLoginFlow: appState.startLoginFlow,
              verifyEmail: appState.verifyEmail,
              signInWithEmailAndPassword: appState.signInWithEmailAndPassword,
              cancelRegistration: appState.cancelRegistration,
              registerAccount: appState.registerAccount,
              signOut: appState.signOut,
            ),
          ),
          const Divider(
            height: 8,
            thickness: 1,
            indent: 8,
            endIndent: 8,
            color: Colors.grey,
          ),
          const Header("What we'll be doing"),
          const Paragraph(
            'Join us for a day full of Firebase Workshops and Pizza!',
          ),
          // Modify from here
          Consumer<ApplicationState>(
            builder: (context, appState, _) => Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                if (appState.loginState == ApplicationLoginState.loggedIn) ...[
                  const Header('Discussion'),
                  GuestBook(
                    addMessage: (message) =>
                        appState.addMessageToGuestBook(message),
                  ),
                ],
              ],
            ),
          ),
          // To here.
        ],
      ),
    );
  }
}

Vous avez remplacé les deux lignes que vous avez ajoutées au début de cette étape par l'implémentation complète. Vous utilisez à nouveau Consumer<ApplicationState> pour rendre l'état de l'application disponible pour la partie de l'arborescence que vous rendez. Cela vous permet de réagir à la saisie d'un message dans l'interface utilisateur et de le publier dans la base de données. Dans la section suivante, vous testerez si les messages ajoutés sont publiés dans la base de données.

Tester l'envoi de messages

  1. Assurez-vous que vous êtes connecté à l'application.
  2. Entrez un message tel que "Salut !", puis cliquez sur ENVOYER .

Cette action écrit le message dans votre base de données Cloud Firestore. Cependant, vous ne verrez pas encore le message dans votre application Flutter réelle car vous devez toujours implémenter la récupération des données. Vous le ferez à l'étape suivante.

Mais vous pouvez voir le message nouvellement ajouté dans la console Firebase.

Dans la console Firebase, dans le tableau de bord de la base de données, vous devriez voir la collection de livres d' guestbook avec votre message nouvellement ajouté. Si vous continuez à envoyer des messages, votre collection de livres d'or contiendra de nombreux documents, comme celui-ci :

Console Firebase

713870af0b3b63c.png

7. Lire les messages

C'est bien que les invités puissent écrire des messages dans la base de données, mais ils ne peuvent pas encore les voir dans l'application. Réparons ça !

Synchroniser les messages

Pour afficher les messages, vous devez ajouter des écouteurs qui se déclenchent lorsque les données changent, puis créer un élément d'interface utilisateur qui affiche les nouveaux messages. Vous allez ajouter du code à l'état de l'application qui écoute les nouveaux messages ajoutés à partir de l'application.

Juste au-dessus du widget GuestBook , la classe de valeur suivante. Cette classe expose une vue structurée des données que vous stockez dans Cloud Firestore.

lib/main.dart

class GuestBookMessage {
  GuestBookMessage({required this.name, required this.message});
  final String name;
  final String message;
}

Dans la section d' ApplicationState où vous définissez l'état et les getters, ajoutez les nouvelles lignes suivantes :

lib/main.dart

  ApplicationLoginState _loginState = ApplicationLoginState.loggedOut;
  ApplicationLoginState get loginState => _loginState;

  String? _email;
  String? get email => _email;

  // Add from here
  StreamSubscription<QuerySnapshot>? _guestBookSubscription;
  List<GuestBookMessage> _guestBookMessages = [];
  List<GuestBookMessage> get guestBookMessages => _guestBookMessages;
  // to here.

Et enfin, dans la section d'initialisation de ApplicationState , ajoutez ce qui suit pour vous abonner à une requête sur la collection de documents lorsqu'un utilisateur se connecte et vous désabonner lorsqu'il se déconnecte.

lib/main.dart

  Future<void> init() async {
    await Firebase.initializeApp(
      options: DefaultFirebaseOptions.currentPlatform,
    );

    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loginState = ApplicationLoginState.loggedIn;
        // Add from here
        _guestBookSubscription = FirebaseFirestore.instance
            .collection('guestbook')
            .orderBy('timestamp', descending: true)
            .snapshots()
            .listen((snapshot) {
          _guestBookMessages = [];
          for (final document in snapshot.docs) {
            _guestBookMessages.add(
              GuestBookMessage(
                name: document.data()['name'] as String,
                message: document.data()['text'] as String,
              ),
            );
          }
          notifyListeners();
        });
        // to here.
      } else {
        _loginState = ApplicationLoginState.loggedOut;
        // Add from here
        _guestBookMessages = [];
        _guestBookSubscription?.cancel();
        // to here.
      }
      notifyListeners();
    });
  }

Cette section est importante, car c'est ici que vous construisez une requête sur la collection de livres d' guestbook et que vous gérez l'abonnement et le désabonnement à cette collection. Vous écoutez le flux, où vous reconstruisez un cache local des messages dans la collection de livres d' guestbook , et stockez également une référence à cet abonnement afin que vous puissiez vous en désabonner ultérieurement. Il se passe beaucoup de choses ici, et cela vaut la peine de passer du temps dans un débogueur à inspecter ce qui se passe pour obtenir un modèle mental plus clair.

Pour plus d'informations, consultez la documentation Cloud Firestore .

Dans le widget GuestBook , vous devez connecter cet état changeant à l'interface utilisateur. Vous modifiez le widget en ajoutant une liste de messages dans le cadre de sa configuration.

lib/main.dart

class GuestBook extends StatefulWidget {
  // Modify the following line
  const GuestBook({required this.addMessage, required this.messages});
  final FutureOr<void> Function(String message) addMessage;
  final List<GuestBookMessage> messages; // new

  @override
  _GuestBookState createState() => _GuestBookState();
}

Ensuite, nous exposons cette nouvelle configuration dans _GuestBookState en modifiant la méthode de build comme suit.

lib/main.dart

class _GuestBookState extends State<GuestBook> {
  final _formKey = GlobalKey<FormState>(debugLabel: '_GuestBookState');
  final _controller = TextEditingController();

  @override
  // Modify from here
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // to here.
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: Form(
            key: _formKey,
            child: Row(
              children: [
                Expanded(
                  child: TextFormField(
                    controller: _controller,
                    decoration: const InputDecoration(
                      hintText: 'Leave a message',
                    ),
                    validator: (value) {
                      if (value == null || value.isEmpty) {
                        return 'Enter your message to continue';
                      }
                      return null;
                    },
                  ),
                ),
                const SizedBox(width: 8),
                StyledButton(
                  onPressed: () async {
                    if (_formKey.currentState!.validate()) {
                      await widget.addMessage(_controller.text);
                      _controller.clear();
                    }
                  },
                  child: Row(
                    children: const [
                      Icon(Icons.send),
                      SizedBox(width: 4),
                      Text('SEND'),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
        // Modify from here
        const SizedBox(height: 8),
        for (var message in widget.messages)
          Paragraph('${message.name}: ${message.message}'),
        const SizedBox(height: 8),
      ],
      // to here.
    );
  }
}

Vous encapsulez le contenu précédent de la méthode build avec un widget Column , puis à la fin des enfants de Column , vous ajoutez une collection pour générer un nouveau Paragraph pour chaque message de la liste des messages.

Enfin, vous devez maintenant mettre à jour le corps de HomePage pour construire correctement GuestBook avec le nouveau paramètre messages .

lib/main.dart

Consumer<ApplicationState>(
  builder: (context, appState, _) => Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      if (appState.loginState == ApplicationLoginState.loggedIn) ...[
        const Header('Discussion'),
        GuestBook(
          addMessage: (message) =>
              appState.addMessageToGuestBook(message),
          messages: appState.guestBookMessages, // new
        ),
      ],
    ],
  ),
),

Tester la synchronisation des messages

Cloud Firestore synchronise automatiquement et instantanément les données avec les clients abonnés à la base de données.

  1. Les messages que vous avez créés précédemment dans la base de données doivent être affichés dans l'application. N'hésitez pas à écrire de nouveaux messages; ils devraient apparaître instantanément.
  2. Si vous ouvrez votre espace de travail dans plusieurs fenêtres ou onglets, les messages seront synchronisés en temps réel entre les onglets.
  3. (Facultatif) Vous pouvez essayer de supprimer, de modifier ou d'ajouter manuellement de nouveaux messages directement dans la section Base de données de la console Firebase ; toute modification doit apparaître dans l'interface utilisateur.

Toutes nos félicitations! Vous lisez des documents Cloud Firestore dans votre application !

Examen de l' application p

8. Mettre en place des règles de sécurité de base

Vous avez initialement configuré Cloud Firestore pour utiliser le mode test, ce qui signifie que votre base de données est ouverte en lecture et en écriture. Cependant, vous ne devez utiliser le mode test qu'aux toutes premières étapes du développement. La meilleure pratique consiste à configurer des règles de sécurité pour votre base de données lorsque vous développez votre application. La sécurité doit faire partie intégrante de la structure et du comportement de votre application.

Les règles de sécurité vous permettent de contrôler l'accès aux documents et aux collections de votre base de données. La syntaxe des règles flexibles vous permet de créer des règles qui correspondent à n'importe quoi, de toutes les écritures dans l'ensemble de la base de données aux opérations sur un document spécifique.

Vous pouvez écrire des règles de sécurité pour Cloud Firestore dans la console Firebase :

  1. Dans la section Développer de la console Firebase, cliquez sur Base de données , puis sélectionnez l'onglet Règles (ou cliquez ici pour accéder directement à l'onglet Règles ).
  2. Vous devriez voir les règles de sécurité par défaut suivantes, ainsi qu'un avertissement indiquant que les règles sont publiques.

7767a2d2e64e7275.png

Identifier les collections

Tout d'abord, identifiez les collections dans lesquelles l'application écrit des données.

Dans match /databases/{database}/documents , identifiez la collection que vous souhaitez sécuriser :

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /guestbook/{entry} {
     // You'll add rules here in the next step.
  }
}

Ajouter des règles de sécurité

Étant donné que vous avez utilisé l'UID d'authentification comme champ dans chaque document de livre d'or, vous pouvez obtenir l'UID d'authentification et vérifier que toute personne tentant d'écrire dans le document dispose d'un UID d'authentification correspondant.

Ajoutez les règles de lecture et d'écriture à votre ensemble de règles comme indiqué ci-dessous :

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /guestbook/{entry} {
      allow read: if request.auth.uid != null;
      allow write:
        if request.auth.uid == request.resource.data.userId;
    }
  }
}

Désormais, pour le livre d'or, seuls les utilisateurs connectés peuvent lire les messages (n'importe quel message !), mais seul l'auteur d'un message peut modifier un message.

Ajouter des règles de validation

Ajoutez la validation des données pour vous assurer que tous les champs attendus sont présents dans le document :

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /guestbook/{entry} {
      allow read: if request.auth.uid != null;
      allow write:
      if request.auth.uid == request.resource.data.userId
          && "name" in request.resource.data
          && "text" in request.resource.data
          && "timestamp" in request.resource.data;
    }
  }
}

9. Étape bonus : pratiquez ce que vous avez appris

Enregistrer le statut RSVP d'un participant

Pour le moment, votre application permet simplement aux utilisateurs de commencer à discuter s'ils sont intéressés par l'événement. De plus, la seule façon de savoir si quelqu'un vient est de le publier dans le chat. Organisons-nous et informons les gens du nombre de personnes qui viennent.

Vous allez ajouter quelques nouvelles fonctionnalités à l'état de l'application. Le premier est la possibilité pour un utilisateur connecté de désigner s'il participe ou non. La deuxième capacité est un compteur du nombre de personnes réellement présentes.

Dans lib/main.dart , ajoutez ce qui suit à la section des accesseurs pour permettre au code de l'interface utilisateur d'interagir avec cet état :

lib/main.dart

int _attendees = 0;
int get attendees => _attendees;

Attending _attending = Attending.unknown;
StreamSubscription<DocumentSnapshot>? _attendingSubscription;
Attending get attending => _attending;
set attending(Attending attending) {
  final userDoc = FirebaseFirestore.instance
      .collection('attendees')
      .doc(FirebaseAuth.instance.currentUser!.uid);
  if (attending == Attending.yes) {
    userDoc.set(<String, dynamic>{'attending': true});
  } else {
    userDoc.set(<String, dynamic>{'attending': false});
  }
}

Mettez à jour la méthode init de ApplicationState comme suit :

lib/main.dart

  Future<void> init() async {
    await Firebase.initializeApp(
      options: DefaultFirebaseOptions.currentPlatform,
    );

    // Add from here
    FirebaseFirestore.instance
        .collection('attendees')
        .where('attending', isEqualTo: true)
        .snapshots()
        .listen((snapshot) {
      _attendees = snapshot.docs.length;
      notifyListeners();
    });
    // To here

    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loginState = ApplicationLoginState.loggedIn;
        _guestBookSubscription = FirebaseFirestore.instance
            .collection('guestbook')
            .orderBy('timestamp', descending: true)
            .snapshots()
            .listen((snapshot) {
          _guestBookMessages = [];
          for (final document in snapshot.docs) {
            _guestBookMessages.add(
              GuestBookMessage(
                name: document.data()['name'] as String,
                message: document.data()['text'] as String,
              ),
            );
          }
          notifyListeners();
        });
        // Add from here
        _attendingSubscription = FirebaseFirestore.instance
            .collection('attendees')
            .doc(user.uid)
            .snapshots()
            .listen((snapshot) {
          if (snapshot.data() != null) {
            if (snapshot.data()!['attending'] as bool) {
              _attending = Attending.yes;
            } else {
              _attending = Attending.no;
            }
          } else {
            _attending = Attending.unknown;
          }
          notifyListeners();
        });
        // to here
      } else {
        _loginState = ApplicationLoginState.loggedOut;
        _guestBookMessages = [];
        _guestBookSubscription?.cancel();
        _attendingSubscription?.cancel(); // new
      }
      notifyListeners();
    });
  }

Ce qui précède ajoute une requête toujours abonnée pour connaître le nombre de participants, et une deuxième requête qui n'est active que lorsqu'un utilisateur est connecté pour savoir si l'utilisateur participe. Ensuite, ajoutez l'énumération suivante après la déclaration GuestBookMessage :

lib/main.dart

enum Attending { yes, no, unknown }

Vous allez maintenant définir un nouveau widget qui agit comme les anciens boutons radio. Il commence dans un état indéterminé, sans oui ni non sélectionné, mais une fois que l'utilisateur sélectionne s'il participe ou non, vous affichez cette option en surbrillance avec un bouton rempli et l'autre option recule avec un rendu plat.

lib/main.dart

class YesNoSelection extends StatelessWidget {
  const YesNoSelection({required this.state, required this.onSelection});
  final Attending state;
  final void Function(Attending selection) onSelection;

  @override
  Widget build(BuildContext context) {
    switch (state) {
      case Attending.yes:
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            children: [
              ElevatedButton(
                style: ElevatedButton.styleFrom(elevation: 0),
                onPressed: () => onSelection(Attending.yes),
                child: const Text('YES'),
              ),
              const SizedBox(width: 8),
              TextButton(
                onPressed: () => onSelection(Attending.no),
                child: const Text('NO'),
              ),
            ],
          ),
        );
      case Attending.no:
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            children: [
              TextButton(
                onPressed: () => onSelection(Attending.yes),
                child: const Text('YES'),
              ),
              const SizedBox(width: 8),
              ElevatedButton(
                style: ElevatedButton.styleFrom(elevation: 0),
                onPressed: () => onSelection(Attending.no),
                child: const Text('NO'),
              ),
            ],
          ),
        );
      default:
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            children: [
              StyledButton(
                onPressed: () => onSelection(Attending.yes),
                child: const Text('YES'),
              ),
              const SizedBox(width: 8),
              StyledButton(
                onPressed: () => onSelection(Attending.no),
                child: const Text('NO'),
              ),
            ],
          ),
        );
    }
  }
}

Ensuite, vous devez mettre à jour la méthode de construction de HomePage pour tirer parti de YesNoSelection , permettant à un utilisateur connecté de désigner s'il participe. Vous afficherez également le nombre de participants à cet événement.

lib/main.dart

Consumer<ApplicationState>(
  builder: (context, appState, _) => Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      // Add from here
      if (appState.attendees >= 2)
        Paragraph('${appState.attendees} people going')
      else if (appState.attendees == 1)
        const Paragraph('1 person going')
      else
        const Paragraph('No one going'),
      // To here.
      if (appState.loginState == ApplicationLoginState.loggedIn) ...[
        // Add from here
        YesNoSelection(
          state: appState.attending,
          onSelection: (attending) => appState.attending = attending,
        ),
        // To here.
        const Header('Discussion'),
        GuestBook(
          addMessage: (message) =>
              appState.addMessageToGuestBook(message),
          messages: appState.guestBookMessages,
        ),
      ],
    ],
  ),
),

Ajouter des règles

Étant donné que vous avez déjà configuré certaines règles, les nouvelles données que vous ajoutez avec les boutons seront rejetées. Vous devrez mettre à jour les règles pour autoriser l'ajout à la collection de attendees .

Pour la collection des attendees , puisque vous avez utilisé l'UID d'authentification comme nom de document, vous pouvez le récupérer et vérifier que l' uid de l'émetteur est le même que le document qu'il écrit. Vous autoriserez tout le monde à lire la liste des participants (puisqu'il n'y a pas de données privées), mais seul le créateur devrait pouvoir la mettre à jour.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // ... //
    match /attendees/{userId} {
      allow read: if true;
      allow write: if request.auth.uid == userId;
    }
  }
}

Ajouter des règles de validation

Ajoutez la validation des données pour vous assurer que tous les champs attendus sont présents dans le document :

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // ... //
    match /attendees/{userId} {
      allow read: if true;
      allow write: if request.auth.uid == userId
          && "attending" in request.resource.data;

    }
  }
}

(Facultatif) Vous pouvez maintenant afficher les résultats en cliquant sur les boutons. Accédez à votre tableau de bord Cloud Firestore dans la console Firebase.

Aperçu de l'application

10. Félicitations !

Vous avez utilisé Firebase pour créer une application Web interactive en temps réel !

Ce que nous avons couvert

  • Authentification Firebase
  • Cloud Firestore
  • Règles de sécurité Firebase

Prochaines étapes

  • Vous souhaitez en savoir plus sur les autres produits Firebase ? Peut-être souhaitez-vous stocker des fichiers image que les utilisateurs téléchargent ? Ou envoyer des notifications à vos utilisateurs ? Consultez la documentation Firebase . Vous voulez en savoir plus sur les plugins Flutter pour Firebase ? Consultez FlutterFire pour plus d'informations.
  • Vous voulez en savoir plus sur Cloud Firestore ? Peut-être souhaitez-vous en savoir plus sur les sous-collections et les transactions ? Rendez-vous sur l'atelier de programmation Web Cloud Firestore pour un atelier de programmation approfondi sur Cloud Firestore. Ou consultez cette série YouTube pour découvrir Cloud Firestore !

Apprendre encore plus

Comment c'était?

Nous aimerions vos retours. Merci de remplir un (très) court formulaire ici .

,

1. Avant de commencer

Dans cet atelier de programmation, vous apprendrez certaines des bases de Firebase pour créer des applications mobiles Flutter pour Android et iOS.

Conditions préalables

Cet atelier de programmation suppose que vous connaissez Flutter et que vous avez installé le SDK Flutter et un éditeur .

Ce que vous allez créer

Dans cet atelier de programmation, vous allez créer une application de chat RSVP d'événement et de livre d'or sur Android, iOS, le Web et macOS à l'aide de Flutter. Vous authentifierez les utilisateurs avec Firebase Authentication et synchroniserez les données à l'aide de Cloud Firestore.

Ce dont vous aurez besoin

Vous pouvez exécuter cet atelier de programmation à l'aide de l'un des appareils suivants :

  • Un appareil physique (Android ou iOS) connecté à votre ordinateur et défini en mode développeur.
  • Le simulateur iOS. (Nécessite l'installation des outils Xcode .)
  • L'émulateur Android. (Nécessite une configuration dans Android Studio .)

En plus de ce qui précède, vous aurez également besoin de :

  • Un navigateur de votre choix, tel que Chrome.
  • Un IDE ou un éditeur de texte de votre choix, comme Android Studio ou VS Code configuré avec les plugins Dart et Flutter.
  • La dernière version stable de Flutter (ou beta si vous aimez vivre à la limite).
  • Un compte Google, comme un compte gmail, pour créer et gérer votre projet Firebase.
  • L' outil de ligne de commande firebase , connecté à votre compte gmail.
  • L'exemple de code du codelab. Voir l'étape suivante pour savoir comment obtenir le code.

2. Obtenez l'exemple de code

Commençons par télécharger la version initiale de notre projet depuis GitHub.

Clonez le dépôt GitHub à partir de la ligne de commande :

git clone https://github.com/flutter/codelabs.git flutter-codelabs

Alternativement, si vous avez installé l'outil cli de GitHub :

gh repo clone flutter/codelabs flutter-codelabs

L'exemple de code doit être cloné dans le répertoire flutter-codelabs , qui contient le code d'une collection de codelabs. Le code de ce laboratoire de programmation se trouve dans flutter-codelabs/firebase-get-to-know-flutter .

La structure de répertoire sous flutter-codelabs/firebase-get-to-know-flutter est une série d'instantanés de l'endroit où vous devriez être à la fin de chaque étape nommée. Il s'agit de l'étape 2, donc localiser les fichiers correspondants est aussi simple que :

cd flutter-codelabs/firebase-get-to-know-flutter/step_02

Si vous souhaitez avancer ou voir à quoi quelque chose devrait ressembler après une étape, regardez dans le répertoire nommé d'après l'étape qui vous intéresse.

Importer l'application de démarrage

Ouvrez ou importez le flutter-codelabs/firebase-get-to-know-flutter/step_02 dans votre IDE préféré. Ce répertoire contient le code de démarrage du laboratoire de programmation qui consiste en une application de rencontre Flutter pas encore fonctionnelle.

Localisez les fichiers sur lesquels travailler

Le code de cette application est réparti sur plusieurs répertoires. Cette répartition des fonctionnalités est conçue pour faciliter le travail, en regroupant le code par fonctionnalité.

Localisez les fichiers suivants dans le projet :

  • lib/main.dart : Ce fichier contient le point d'entrée principal et le widget de l'application.
  • lib/src/widgets.dart : Ce fichier contient une poignée de widgets pour aider à standardiser le style de l'application. Ceux-ci sont utilisés pour composer l'écran de l'application de démarrage.
  • lib/src/authentication.dart : ce fichier contient une implémentation partielle de FirebaseUI Auth avec un ensemble de widgets pour créer une expérience utilisateur de connexion pour l'authentification basée sur les e-mails Firebase. Ces widgets pour le flux d'authentification ne sont pas encore utilisés dans l'application de démarrage, mais vous les connecterez bientôt.

Vous ajouterez des fichiers supplémentaires si nécessaire pour développer le reste de l'application.

Examen du fichier lib/main.dart

Cette application tire parti du package google_fonts pour nous permettre de faire de Roboto la police par défaut dans toute l'application. Un exercice pour le lecteur motivé consiste à explorer fonts.google.com et à utiliser les polices que vous y découvrez dans différentes parties de l'application.

Vous utilisez les widgets d'assistance de lib/src/widgets.dart sous la forme de Header , Paragraph et IconAndDetail . Ces widgets réduisent l'encombrement de la mise en page décrite dans HomePage en éliminant le code dupliqué. Cela a l'avantage supplémentaire de permettre une apparence et une sensation cohérentes.

Voici à quoi ressemble votre application sur Android, iOS, le Web et macOS :

Aperçu de l'application

3. Créer et configurer un projet Firebase

L'affichage des informations sur l'événement est idéal pour vos invités, mais le simple fait d'afficher les événements n'est très utile pour personne. Ajoutons quelques fonctionnalités dynamiques à cette application. Pour cela, vous devrez connecter Firebase à votre application. Pour démarrer avec Firebase, vous devez créer et configurer un projet Firebase.

Créer un projet Firebase

  1. Connectez-vous à Firebase .
  2. Dans la console Firebase, cliquez sur Ajouter un projet (ou Créer un projet ) et nommez votre projet Firebase Firebase-Flutter-Codelab .

4395e4e67c08043a.png

  1. Cliquez sur les options de création de projet. Acceptez les conditions de Firebase si vous y êtes invité. Ignorez la configuration de Google Analytics, car vous n'utiliserez pas Analytics pour cette application.

b7138cde5f2c7b61.png

Pour en savoir plus sur les projets Firebase, consultez Comprendre les projets Firebase .

L'application que vous créez utilise plusieurs produits Firebase disponibles pour les applications Web :

  • Firebase Authentication pour permettre à vos utilisateurs de se connecter à votre application.
  • Cloud Firestore pour enregistrer des données structurées sur le cloud et recevoir une notification instantanée lorsque les données changent.
  • Règles de sécurité Firebase pour sécuriser votre base de données.

Certains de ces produits nécessitent une configuration spéciale ou doivent être activés à l'aide de la console Firebase.

Activer la connexion par e-mail pour l'authentification Firebase

Pour autoriser les utilisateurs à se connecter à l'application Web, vous utiliserez la méthode de connexion par e-mail/mot de passe pour cet atelier de programmation :

  1. Dans la console Firebase, développez le menu Build dans le panneau de gauche.
  2. Cliquez sur Authentification , puis cliquez sur le bouton Commencer , puis sur l'onglet Méthode de connexion (ou cliquez ici pour accéder directement à l'onglet Méthode de connexion).
  3. Cliquez sur E-mail/Mot de passe dans la liste Fournisseurs de connexion, réglez le commutateur Activer sur la position Activé, puis cliquez sur Enregistrer . 58e3e3e23c2f16a4.png

Activer Cloud Firestore

L'application Web utilise Cloud Firestore pour enregistrer les messages de chat et recevoir de nouveaux messages de chat.

Activez Cloud Firestore :

  1. Dans la section Build de la console Firebase, cliquez sur Cloud Firestore .
  2. Cliquez sur Créer une base de données . 99e8429832d23fa3.png
  1. Select the Start in test mode option. Read the disclaimer about the security rules. Test mode ensures that you can freely write to the database during development. Click Next . 6be00e26c72ea032.png
  1. Select the location for your database (You can just use the default). Note that this location can't be changed later. 278656eefcfb0216.png
  2. Click Enable .

4. Firebase configuration

In order to use Firebase with Flutter, you need to follow a process to configure the Flutter project to utilise the FlutterFire libraries correctly:

  • Add the FlutterFire dependencies to your project
  • Register the desired platform on the Firebase project
  • Download the platform-specific configuration file, and add it to the code.

In the top-level directory of your Flutter app, there are subdirectories called android , ios , macos and web . These directories hold the platform-specific configuration files for iOS and Android, respectively.

Configure dependencies

You need to add the FlutterFire libraries for the two Firebase products you are utilizing in this app - Firebase Auth and Cloud Firestore. Run the following three commands to add the depencies.

$ flutter pub add firebase_core 
Resolving dependencies...
+ firebase_core 1.10.5
+ firebase_core_platform_interface 4.2.2
+ firebase_core_web 1.5.2
+ flutter_web_plugins 0.0.0 from sdk flutter
+ js 0.6.3
  test_api 0.4.3 (0.4.8 available)
Changed 5 dependencies!

The firebase_core is the common code required for all Firebase Flutter plugins.

$ flutter pub add firebase_auth
Resolving dependencies...
+ firebase_auth 3.3.3
+ firebase_auth_platform_interface 6.1.8
+ firebase_auth_web 3.3.4
+ intl 0.17.0
  test_api 0.4.3 (0.4.8 available)
Changed 4 dependencies!

The firebase_auth enables integration with Firebase's Authentication capability.

$ flutter pub add cloud_firestore
Resolving dependencies...
+ cloud_firestore 3.1.4
+ cloud_firestore_platform_interface 5.4.9
+ cloud_firestore_web 2.6.4
  test_api 0.4.3 (0.4.8 available)
Changed 3 dependencies!

The cloud_firestore enables access to Cloud Firestore data storage.

$ flutter pub add provider
Resolving dependencies...
+ nested 1.0.0
+ provider 6.0.1
  test_api 0.4.3 (0.4.8 available)
Changed 2 dependencies!

While you have added the required packages, you also need to configure the iOS, Android, macOS and Web runner projects to appropriately utilise Firebase. You are also using the provider package that will enable separation of business logic from display logic.

Installing flutterfire

The FlutterFire CLI depends on the underlying Firebase CLI. If you haven't done so already, ensure the Firebase CLI is installed on your machine.

Next, install the FlutterFire CLI by running the following command:

$ dart pub global activate flutterfire_cli

Once installed, the flutterfire command will be globally available.

Configuring your apps

The CLI extracts information from your Firebase project and selected project applications to generate all the configuration for a specific platform.

In the root of your application, run the configure command:

$ flutterfire configure

The configuration command will guide you through a number of processes:

  1. Selecting a Firebase project (based on the .firebaserc file or from the Firebase Console).
  2. Prompt what platforms (eg Android, iOS, macOS & web) you would like configuration for.
  3. Identify which Firebase applications for the chosen platforms should be used to extract configuration for. By default, the CLI will attempt to automatically match Firebase apps based on your current project configuration.
  4. Generate a firebase_options.dart file in your project.

Configure macOS

Flutter on macOS builds fully sandboxed applications. As this application is integrating using the network to communicate with the Firebase servers, you will need to configure your application with network client privileges.

macos/Runner/DebugProfile.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.app-sandbox</key>
	<true/>
	<key>com.apple.security.cs.allow-jit</key>
	<true/>
	<key>com.apple.security.network.server</key>
	<true/>
  <!-- Add the following two lines -->
	<key>com.apple.security.network.client</key>
	<true/>
</dict>
</plist>

macos/Runner/Release.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.app-sandbox</key>
	<true/>
  <!-- Add the following two lines -->
	<key>com.apple.security.network.client</key>
	<true/>
</dict>
</plist>

See Entitlements and the App Sandbox for more detail.

5. Add user sign-in (RSVP)

Now that you've added Firebase to the app, you can set up an RSVP button that registers people using Firebase Authentication . For Android native, iOS native, and Web there are pre-built FirebaseUI Auth packages, but for Flutter you will need to build this capability.

The project you retrieved in Step 2 included a set of widgets that implements the user interface for most of the authentication flow. You will implement the business logic to integrate Firebase Authentication into the application.

Business Logic with Provider

You are going to use the provider package to make a centralized application state object available throughout the application's tree of Flutter widgets. To start with, modify the imports at the top of lib/main.dart :

lib/main.dart

import 'package:firebase_auth/firebase_auth.dart'; // new
import 'package:firebase_core/firebase_core.dart'; // new
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';           // new

import 'firebase_options.dart';                    // new
import 'src/authentication.dart';                  // new
import 'src/widgets.dart';

The import lines introduce Firebase Core and Auth, pull in the provider package which you are using to make the application state object available through the widget tree, and include the authentication widgets from lib/src .

This application state object, ApplicationState , has two main responsibilities for this step, but will gain additional responsibilities as you add more capabilities to the application in later steps. The first responsibility is to initialize the Firebase library with a call to Firebase.initializeApp() , and then there is the handling of the authorization flow. Add the following class to the end of lib/main.dart :

lib/main.dart

class ApplicationState extends ChangeNotifier {
  ApplicationState() {
    init();
  }

  Future<void> init() async {
    await Firebase.initializeApp(
      options: DefaultFirebaseOptions.currentPlatform,
    );

    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loginState = ApplicationLoginState.loggedIn;
      } else {
        _loginState = ApplicationLoginState.loggedOut;
      }
      notifyListeners();
    });
  }

  ApplicationLoginState _loginState = ApplicationLoginState.loggedOut;
  ApplicationLoginState get loginState => _loginState;

  String? _email;
  String? get email => _email;

  void startLoginFlow() {
    _loginState = ApplicationLoginState.emailAddress;
    notifyListeners();
  }

  Future<void> verifyEmail(
    String email,
    void Function(FirebaseAuthException e) errorCallback,
  ) async {
    try {
      var methods =
          await FirebaseAuth.instance.fetchSignInMethodsForEmail(email);
      if (methods.contains('password')) {
        _loginState = ApplicationLoginState.password;
      } else {
        _loginState = ApplicationLoginState.register;
      }
      _email = email;
      notifyListeners();
    } on FirebaseAuthException catch (e) {
      errorCallback(e);
    }
  }

  Future<void> signInWithEmailAndPassword(
    String email,
    String password,
    void Function(FirebaseAuthException e) errorCallback,
  ) async {
    try {
      await FirebaseAuth.instance.signInWithEmailAndPassword(
        email: email,
        password: password,
      );
    } on FirebaseAuthException catch (e) {
      errorCallback(e);
    }
  }

  void cancelRegistration() {
    _loginState = ApplicationLoginState.emailAddress;
    notifyListeners();
  }

  Future<void> registerAccount(
      String email,
      String displayName,
      String password,
      void Function(FirebaseAuthException e) errorCallback) async {
    try {
      var credential = await FirebaseAuth.instance
          .createUserWithEmailAndPassword(email: email, password: password);
      await credential.user!.updateDisplayName(displayName);
    } on FirebaseAuthException catch (e) {
      errorCallback(e);
    }
  }

  void signOut() {
    FirebaseAuth.instance.signOut();
  }
}

It is worth noting a few key points in this class. The user starts off unauthenticated, the app shows a form requesting the user's email address, depending on whether that email address is on file, the app will either ask the user register, or request their password, and then assuming everything works out, the user is authenticated.

It must be noted that this isn't a complete implementation of the FirebaseUI Auth flow, as it does not handle the case of a user with an existing account who is having trouble logging in. Implementing this additional capability is left as an exercise to the motivated reader.

Integrating the Authentication flow

Now that you have the start of the application state it is time to wire the application state into the app initialization and add the authentication flow into HomePage . Update the main entry point to integrate application state via the provider package:

lib/main.dart

void main() {
  // Modify from here
  runApp(
    ChangeNotifierProvider(
      create: (context) => ApplicationState(),
      builder: (context, _) => App(),
    ),
  );
  // to here.
}

The modification to the main function makes the provider package responsible for instantiating the application state object using the ChangeNotifierProvider widget. You are using this specific provider class because the application state object extends ChangeNotifier and this enables the provider package to know when to redisplay dependent widgets. Finally, integrate the application state with Authentication by updating HomePage 's build method:

lib/main.dart

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Firebase Meetup'),
      ),
      body: ListView(
        children: <Widget>[
          Image.asset('assets/codelab.png'),
          const SizedBox(height: 8),
          const IconAndDetail(Icons.calendar_today, 'October 30'),
          const IconAndDetail(Icons.location_city, 'San Francisco'),
          // Add from here
          Consumer<ApplicationState>(
            builder: (context, appState, _) => Authentication(
              email: appState.email,
              loginState: appState.loginState,
              startLoginFlow: appState.startLoginFlow,
              verifyEmail: appState.verifyEmail,
              signInWithEmailAndPassword: appState.signInWithEmailAndPassword,
              cancelRegistration: appState.cancelRegistration,
              registerAccount: appState.registerAccount,
              signOut: appState.signOut,
            ),
          ),
          // to here
          const Divider(
            height: 8,
            thickness: 1,
            indent: 8,
            endIndent: 8,
            color: Colors.grey,
          ),
          const Header("What we'll be doing"),
          const Paragraph(
            'Join us for a day full of Firebase Workshops and Pizza!',
          ),
        ],
      ),
    );
  }
}

You instantiate the Authentication widget, and wrap it in a Consumer widget. The Consumer widget the usual way that the provider package can be used to rebuild part of the tree when the application state changes. The Authentication widget is the authentication UI that you will now test.

Testing the Authentication flow

cdf2d25e436bd48d.png

Here is the start of the authentication flow, where the user can tap on the RSVP button, to initiate the email form.

2a2cd6d69d172369.png

Upon entering the email, the system confirms if the user is already registered, in which case the user is prompted for a password, alternatively if the user isn't registered, then they go through the registration form.

e5e65065dba36b54.png

Make sure to try out entering a short password (less than six characters) to check the error handling flow. If the user is registered, they will see the password for instead.

fbb3ea35fb4f67a.png

On this page make sure to enter incorrect passwords to check the error handling on this page. Finally, once the user is logged in, you will see the logged in experience which offers the user the ability to log out again.

4ed811a25b0cf816.png

And with that, you have implemented an authentication flow. Félicitations!

6. Write messages to Cloud Firestore

Knowing that users are coming is great, but let's give the guests something else to do in the app. What if they could leave messages in a guestbook? They can share why they're excited to come or who they hope to meet.

To store the chat messages that users write in the app, you'll use Cloud Firestore .

Modèle de données

Cloud Firestore is a NoSQL database, and data stored in the database is split into collections, documents, fields, and subcollections. You will store each message of the chat as a document in a top-level collection called guestbook .

7c20dc8424bb1d84.png

Add messages to Firestore

In this section, you'll add the functionality for users to write new messages to the database. First, you add the UI elements (form field and send button), and then you add the code that hooks these elements up to the database.

First, add imports for the cloud_firestore package and dart:async .

lib/main.dart

import 'dart:async';                                    // new

import 'package:cloud_firestore/cloud_firestore.dart';  // new
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';

import 'firebase_options.dart';
import 'src/authentication.dart';
import 'src/widgets.dart';

To construct the UI elements of a message field and a send button, add a new stateful widget GuestBook at the bottom of lib/main.dart .

lib/main.dart

class GuestBook extends StatefulWidget {
  const GuestBook({required this.addMessage});
  final FutureOr<void> Function(String message) addMessage;

  @override
  _GuestBookState createState() => _GuestBookState();
}

class _GuestBookState extends State<GuestBook> {
  final _formKey = GlobalKey<FormState>(debugLabel: '_GuestBookState');
  final _controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Form(
        key: _formKey,
        child: Row(
          children: [
            Expanded(
              child: TextFormField(
                controller: _controller,
                decoration: const InputDecoration(
                  hintText: 'Leave a message',
                ),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Enter your message to continue';
                  }
                  return null;
                },
              ),
            ),
            const SizedBox(width: 8),
            StyledButton(
              onPressed: () async {
                if (_formKey.currentState!.validate()) {
                  await widget.addMessage(_controller.text);
                  _controller.clear();
                }
              },
              child: Row(
                children: const [
                  Icon(Icons.send),
                  SizedBox(width: 4),
                  Text('SEND'),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

There are a couple of points of interest here. First up, you are instantiating a Form so you can validate the message actually has some content, and show the user an error message if there isn't any. The way to validate a form involves accessing the form state behind the form, and for this you use a GlobalKey . For more information on Keys, and how to use them, please see the Flutter Widgets 101 episode "When to Use Keys" .

Also note the way the widgets are laid out, you have a Row , with a TextFormField and a StyledButton , which itself contains a Row . Also note the TextFormField is wrapped in an Expanded widget, this forces the TextFormField to take up any extra space in the row. To better understand why this is required, please read through Understanding constraints .

Now that you have a widget that enables the user to enter some text to add to the Guest Book, you need to get it on the screen. To do so, edit the body of HomePage to add the following two lines at the bottom of the ListView 's children:

const Header("What we'll be doing"),
const Paragraph(
  'Join us for a day full of Firebase Workshops and Pizza!',
),
// Add the following two lines.
const Header('Discussion'),
GuestBook(addMessage: (message) => print(message)),

While this is enough to display the Widget, it isn't sufficient to do anything useful. You will update this code shortly to make it functional.

App preview

Un utilisateur cliquant sur le bouton ENVOYER déclenchera l'extrait de code ci-dessous. It adds the contents of the message input field to the guestbook collection of the database. Specifically, the addMessageToGuestBook method adds the message content to a new document (with an automatically generated ID) to the guestbook collection.

Note that FirebaseAuth.instance.currentUser.uid is a reference to the auto-generated unique ID that Firebase Authentication gives for all logged-in users.

Make another change to the lib/main.dart file. Add the addMessageToGuestBook method. You will wire the user interface and this capability together in the next step.

lib/main.dart

class ApplicationState extends ChangeNotifier {

  // Current content of ApplicationState elided ...

  // Add from here
  Future<DocumentReference> addMessageToGuestBook(String message) {
    if (_loginState != ApplicationLoginState.loggedIn) {
      throw Exception('Must be logged in');
    }

    return FirebaseFirestore.instance
        .collection('guestbook')
        .add(<String, dynamic>{
      'text': message,
      'timestamp': DateTime.now().millisecondsSinceEpoch,
      'name': FirebaseAuth.instance.currentUser!.displayName,
      'userId': FirebaseAuth.instance.currentUser!.uid,
    });
  }
  // To here
}

Wiring the UI into the database

You have a UI where the user can enter the text they want to add to the Guest Book, and you have the code to add the entry to Cloud Firestore. Now all you need to do is wire the two together. In lib/main.dart make the following change to the HomePage widget.

lib/main.dart

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Firebase Meetup'),
      ),
      body: ListView(
        children: <Widget>[
          Image.asset('assets/codelab.png'),
          const SizedBox(height: 8),
          const IconAndDetail(Icons.calendar_today, 'October 30'),
          const IconAndDetail(Icons.location_city, 'San Francisco'),
          Consumer<ApplicationState>(
            builder: (context, appState, _) => Authentication(
              email: appState.email,
              loginState: appState.loginState,
              startLoginFlow: appState.startLoginFlow,
              verifyEmail: appState.verifyEmail,
              signInWithEmailAndPassword: appState.signInWithEmailAndPassword,
              cancelRegistration: appState.cancelRegistration,
              registerAccount: appState.registerAccount,
              signOut: appState.signOut,
            ),
          ),
          const Divider(
            height: 8,
            thickness: 1,
            indent: 8,
            endIndent: 8,
            color: Colors.grey,
          ),
          const Header("What we'll be doing"),
          const Paragraph(
            'Join us for a day full of Firebase Workshops and Pizza!',
          ),
          // Modify from here
          Consumer<ApplicationState>(
            builder: (context, appState, _) => Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                if (appState.loginState == ApplicationLoginState.loggedIn) ...[
                  const Header('Discussion'),
                  GuestBook(
                    addMessage: (message) =>
                        appState.addMessageToGuestBook(message),
                  ),
                ],
              ],
            ),
          ),
          // To here.
        ],
      ),
    );
  }
}

You have replaced the two lines you added back at the start of this step with the full implementation. You are again using Consumer<ApplicationState> to make the application state available to the part of the tree you are rendering. This enables you to react to someone entering a message in the UI, and publish it into the database. In the next section you will test if the added messages are published into the database.

Test sending messages

  1. Make sure that you're signed in to the app.
  2. Enter a message such as "Hey there!", and then click SEND .

This action writes the message to your Cloud Firestore database. However, you won't yet see the message in your actual Flutter app because you still need to implement retrieving the data. You'll do that in the next step.

But you can see the newly added message in the Firebase console.

In the Firebase console, in the Database dashboard , you should see the guestbook collection with your newly added message. If you keep sending messages, your guestbook collection will contain many documents, like this:

Firebase console

713870af0b3b63c.png

7. Read messages

It's lovely that guests can write messages to the database, but they can't see them in the app yet. Let's fix that!

Synchronize messages

To display messages, you'll need to add listeners that trigger when data changes and then create a UI element that shows new messages. You'll add code to the application state that listens for newly added messages from the app.

Just above the GuestBook widget the following value class. This class exposes a structured view of the data you are storing in Cloud Firestore.

lib/main.dart

class GuestBookMessage {
  GuestBookMessage({required this.name, required this.message});
  final String name;
  final String message;
}

In the section of ApplicationState where you define state and getters, add the following new lines:

lib/main.dart

  ApplicationLoginState _loginState = ApplicationLoginState.loggedOut;
  ApplicationLoginState get loginState => _loginState;

  String? _email;
  String? get email => _email;

  // Add from here
  StreamSubscription<QuerySnapshot>? _guestBookSubscription;
  List<GuestBookMessage> _guestBookMessages = [];
  List<GuestBookMessage> get guestBookMessages => _guestBookMessages;
  // to here.

And finally, in the initialization section of ApplicationState , add the following to subscribe to a query over the document collection when a user logs in, and unsubscribe when they log out.

lib/main.dart

  Future<void> init() async {
    await Firebase.initializeApp(
      options: DefaultFirebaseOptions.currentPlatform,
    );

    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loginState = ApplicationLoginState.loggedIn;
        // Add from here
        _guestBookSubscription = FirebaseFirestore.instance
            .collection('guestbook')
            .orderBy('timestamp', descending: true)
            .snapshots()
            .listen((snapshot) {
          _guestBookMessages = [];
          for (final document in snapshot.docs) {
            _guestBookMessages.add(
              GuestBookMessage(
                name: document.data()['name'] as String,
                message: document.data()['text'] as String,
              ),
            );
          }
          notifyListeners();
        });
        // to here.
      } else {
        _loginState = ApplicationLoginState.loggedOut;
        // Add from here
        _guestBookMessages = [];
        _guestBookSubscription?.cancel();
        // to here.
      }
      notifyListeners();
    });
  }

This section is important, as here is where you construct a query over the guestbook collection, and handle subscribing and unsubscribing to this collection. You listen to the stream, where you reconstruct a local cache of the messages in the guestbook collection, and also store a reference to this subscription so you can unsubscribe from it later. There is a lot going on here, and it is worth spending some time in a debugger inspecting what happens when to get a clearer mental model.

For more information, see the Cloud Firestore documentation .

In the GuestBook widget you need to connect this changing state to the user interface. You modify the widget by adding a list of messages as part of its configuration.

lib/main.dart

class GuestBook extends StatefulWidget {
  // Modify the following line
  const GuestBook({required this.addMessage, required this.messages});
  final FutureOr<void> Function(String message) addMessage;
  final List<GuestBookMessage> messages; // new

  @override
  _GuestBookState createState() => _GuestBookState();
}

Next, we expose this new configuration in _GuestBookState by modifying the build method as follows.

lib/main.dart

class _GuestBookState extends State<GuestBook> {
  final _formKey = GlobalKey<FormState>(debugLabel: '_GuestBookState');
  final _controller = TextEditingController();

  @override
  // Modify from here
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // to here.
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: Form(
            key: _formKey,
            child: Row(
              children: [
                Expanded(
                  child: TextFormField(
                    controller: _controller,
                    decoration: const InputDecoration(
                      hintText: 'Leave a message',
                    ),
                    validator: (value) {
                      if (value == null || value.isEmpty) {
                        return 'Enter your message to continue';
                      }
                      return null;
                    },
                  ),
                ),
                const SizedBox(width: 8),
                StyledButton(
                  onPressed: () async {
                    if (_formKey.currentState!.validate()) {
                      await widget.addMessage(_controller.text);
                      _controller.clear();
                    }
                  },
                  child: Row(
                    children: const [
                      Icon(Icons.send),
                      SizedBox(width: 4),
                      Text('SEND'),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
        // Modify from here
        const SizedBox(height: 8),
        for (var message in widget.messages)
          Paragraph('${message.name}: ${message.message}'),
        const SizedBox(height: 8),
      ],
      // to here.
    );
  }
}

You wrap the previous content of the build method with a Column widget, and then at the tail of the Column 's children you add a collection for to generate a new Paragraph for each message in the list of messages.

Finally, you now need to update the body of HomePage to correctly construct GuestBook with the new messages parameter.

lib/main.dart

Consumer<ApplicationState>(
  builder: (context, appState, _) => Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      if (appState.loginState == ApplicationLoginState.loggedIn) ...[
        const Header('Discussion'),
        GuestBook(
          addMessage: (message) =>
              appState.addMessageToGuestBook(message),
          messages: appState.guestBookMessages, // new
        ),
      ],
    ],
  ),
),

Tester la synchronisation des messages

Cloud Firestore automatically and instantly synchronizes data with clients subscribed to the database.

  1. The messages that you created earlier in the database should be displayed in the app. N'hésitez pas à écrire de nouveaux messages; ils devraient apparaître instantanément.
  2. If you open your workspace in multiple windows or tabs, messages will sync in real time across tabs.
  3. (Facultatif) Vous pouvez essayer de supprimer, de modifier ou d'ajouter manuellement de nouveaux messages directement dans la section Base de données de la console Firebase ; any changes should appear in the UI.

Toutes nos félicitations! Vous lisez des documents Cloud Firestore dans votre application !

App p review

8. Set up basic security rules

You initially set up Cloud Firestore to use test mode, meaning that your database is open for reads and writes. However, you should only use test mode during very early stages of development. As a best practice, you should set up security rules for your database as you develop your app. Security should be integral to your app's structure and behavior.

Security Rules allow you to control access to documents and collections in your database. The flexible rules syntax allows you to create rules that match anything from all writes to the entire database to operations on a specific document.

You can write security rules for Cloud Firestore in the Firebase console:

  1. In the Firebase console's Develop section, click Database , and then select the Rules tab (or click here to go directly to the Rules tab).
  2. You should see the following default security rules, along with a warning about the rules being public.

7767a2d2e64e7275.png

Identify collections

First, identify the collections to which the app writes data.

In match /databases/{database}/documents , identify the collection that you want to secure:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /guestbook/{entry} {
     // You'll add rules here in the next step.
  }
}

Add security rules

Because you used the Authentication UID as a field in each guestbook document, you can get the Authentication UID and verify that anyone attempting to write to the document has a matching Authentication UID.

Add the read and write rules to your rule set as shown below:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /guestbook/{entry} {
      allow read: if request.auth.uid != null;
      allow write:
        if request.auth.uid == request.resource.data.userId;
    }
  }
}

Now, for the guestbook, only signed-in users can read messages (any message!), but only a message's author can edit a message.

Add validation rules

Add data validation to make sure that all of the expected fields are present in the document:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /guestbook/{entry} {
      allow read: if request.auth.uid != null;
      allow write:
      if request.auth.uid == request.resource.data.userId
          && "name" in request.resource.data
          && "text" in request.resource.data
          && "timestamp" in request.resource.data;
    }
  }
}

9. Bonus step: Practice what you've learned

Record an attendee's RSVP status

Right now, your app just allows people to start chatting if they're interested in the event. Also, the only way you know if someone's coming is if they post it in the chat. Let's get organized and let people know how many people are coming.

You are going to add a couple of new capabilities to the application state. The first is the ability for a logged in user to nominate if they are attending or not. The second capability is a counter of how many people are actually attending.

In lib/main.dart , add the following to the accessors section to enable the UI code to interact with this state:

lib/main.dart

int _attendees = 0;
int get attendees => _attendees;

Attending _attending = Attending.unknown;
StreamSubscription<DocumentSnapshot>? _attendingSubscription;
Attending get attending => _attending;
set attending(Attending attending) {
  final userDoc = FirebaseFirestore.instance
      .collection('attendees')
      .doc(FirebaseAuth.instance.currentUser!.uid);
  if (attending == Attending.yes) {
    userDoc.set(<String, dynamic>{'attending': true});
  } else {
    userDoc.set(<String, dynamic>{'attending': false});
  }
}

Update ApplicationState 's init method as follows:

lib/main.dart

  Future<void> init() async {
    await Firebase.initializeApp(
      options: DefaultFirebaseOptions.currentPlatform,
    );

    // Add from here
    FirebaseFirestore.instance
        .collection('attendees')
        .where('attending', isEqualTo: true)
        .snapshots()
        .listen((snapshot) {
      _attendees = snapshot.docs.length;
      notifyListeners();
    });
    // To here

    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loginState = ApplicationLoginState.loggedIn;
        _guestBookSubscription = FirebaseFirestore.instance
            .collection('guestbook')
            .orderBy('timestamp', descending: true)
            .snapshots()
            .listen((snapshot) {
          _guestBookMessages = [];
          for (final document in snapshot.docs) {
            _guestBookMessages.add(
              GuestBookMessage(
                name: document.data()['name'] as String,
                message: document.data()['text'] as String,
              ),
            );
          }
          notifyListeners();
        });
        // Add from here
        _attendingSubscription = FirebaseFirestore.instance
            .collection('attendees')
            .doc(user.uid)
            .snapshots()
            .listen((snapshot) {
          if (snapshot.data() != null) {
            if (snapshot.data()!['attending'] as bool) {
              _attending = Attending.yes;
            } else {
              _attending = Attending.no;
            }
          } else {
            _attending = Attending.unknown;
          }
          notifyListeners();
        });
        // to here
      } else {
        _loginState = ApplicationLoginState.loggedOut;
        _guestBookMessages = [];
        _guestBookSubscription?.cancel();
        _attendingSubscription?.cancel(); // new
      }
      notifyListeners();
    });
  }

The above adds an always subscribed query to find out the number of attendees, and a second query that is only active while a user is logged in to find out if the user is attending. Next, add the following enumeration after the GuestBookMessage declaration:

lib/main.dart

enum Attending { yes, no, unknown }

You are now going to define a new widget that acts like radio buttons of old. It starts off in an indeterminate state, with neither yes nor no selected, but once the user selects whether they are attending or not, then you show that option highlighted with a filled button, and the other option receding with a flat rendering.

lib/main.dart

class YesNoSelection extends StatelessWidget {
  const YesNoSelection({required this.state, required this.onSelection});
  final Attending state;
  final void Function(Attending selection) onSelection;

  @override
  Widget build(BuildContext context) {
    switch (state) {
      case Attending.yes:
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            children: [
              ElevatedButton(
                style: ElevatedButton.styleFrom(elevation: 0),
                onPressed: () => onSelection(Attending.yes),
                child: const Text('YES'),
              ),
              const SizedBox(width: 8),
              TextButton(
                onPressed: () => onSelection(Attending.no),
                child: const Text('NO'),
              ),
            ],
          ),
        );
      case Attending.no:
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            children: [
              TextButton(
                onPressed: () => onSelection(Attending.yes),
                child: const Text('YES'),
              ),
              const SizedBox(width: 8),
              ElevatedButton(
                style: ElevatedButton.styleFrom(elevation: 0),
                onPressed: () => onSelection(Attending.no),
                child: const Text('NO'),
              ),
            ],
          ),
        );
      default:
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            children: [
              StyledButton(
                onPressed: () => onSelection(Attending.yes),
                child: const Text('YES'),
              ),
              const SizedBox(width: 8),
              StyledButton(
                onPressed: () => onSelection(Attending.no),
                child: const Text('NO'),
              ),
            ],
          ),
        );
    }
  }
}

Next, you need to update HomePage 's build method to take advantage of YesNoSelection , enabling a logged in user to nominate if they are attending. You will also display the number of attendees for this event.

lib/main.dart

Consumer<ApplicationState>(
  builder: (context, appState, _) => Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      // Add from here
      if (appState.attendees >= 2)
        Paragraph('${appState.attendees} people going')
      else if (appState.attendees == 1)
        const Paragraph('1 person going')
      else
        const Paragraph('No one going'),
      // To here.
      if (appState.loginState == ApplicationLoginState.loggedIn) ...[
        // Add from here
        YesNoSelection(
          state: appState.attending,
          onSelection: (attending) => appState.attending = attending,
        ),
        // To here.
        const Header('Discussion'),
        GuestBook(
          addMessage: (message) =>
              appState.addMessageToGuestBook(message),
          messages: appState.guestBookMessages,
        ),
      ],
    ],
  ),
),

Add rules

Because you already have some rules set up, the new data that you're adding with the buttons is going to be rejected. You'll need to update the rules to allow adding to the attendees collection.

For the attendees collection, since you used the Authentication UID as the document name, you can grab it and verify that the submitter's uid is the same as the document they are writing. You'll allow everyone to read the attendees list (since there is no private data there), but only the creator should be able to update it.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // ... //
    match /attendees/{userId} {
      allow read: if true;
      allow write: if request.auth.uid == userId;
    }
  }
}

Add validation rules

Add data validation to make sure that all of the expected fields are present in the document:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // ... //
    match /attendees/{userId} {
      allow read: if true;
      allow write: if request.auth.uid == userId
          && "attending" in request.resource.data;

    }
  }
}

(Optional) You can now view the results of clicking the buttons. Go to your Cloud Firestore dashboard in the Firebase console.

App preview

10. Congratulations!

You've used Firebase to build an interactive, real-time web application!

Ce que nous avons couvert

  • Authentification Firebase
  • Cloud Firestore
  • Firebase Security Rules

Prochaines étapes

  • Want to learn more about other Firebase products? Maybe you want to store image files that users upload? Or send notifications to your users? Check out the Firebase documentation . Want to learn more about Flutter plugins for Firebase? Check out FlutterFire for more information.
  • Want to learn more about Cloud Firestore? Maybe you want to learn about subcollections and transactions? Head over to the Cloud Firestore web codelab for a codelab that goes into more depth on Cloud Firestore. Or check out this YouTube series to get to know Cloud Firestore !

Learn more

How did it go?

We would love your feedback! Please fill out a (very) short form here .