Découvrir Firebase pour Flutter

1. Avant de commencer

Dans cet atelier de programmation, vous allez découvrir les bases de Firebase pour créer des applications mobiles Flutter pour Android et iOS.

Prérequis

Points abordés

  • Découvrez comment créer une application de chat pour les RSVP et le livre d'or d'un événement sur Android, iOS, le Web et macOS avec Flutter.
  • Découvrez comment authentifier des utilisateurs avec Firebase Authentication et synchroniser des données avec Firestore.

Écran d'accueil de l'application sur Android

Écran d'accueil de l'application sur iOS

Prérequis

L'un des appareils suivants :

  • Un appareil Android ou iOS physique connecté à votre ordinateur et réglé en mode développeur
  • Le simulateur iOS (outils Xcode requis)
  • L'émulateur Android (qui doit être configuré dans Android Studio)

Vous devez également disposer des éléments suivants :

  • Un navigateur de votre choix, tel que Google Chrome.
  • Un IDE ou un éditeur de texte de votre choix configuré avec les plug-ins Dart et Flutter, tel qu'Android Studio ou Visual Studio Code.
  • La dernière version stable de Flutter ou beta si vous aimez prendre des risques.
  • Un compte Google pour créer et gérer votre projet Firebase.
  • La CLI Firebase est connectée à votre compte Google.

2. Obtenir l'exemple de code

Téléchargez la version initiale de votre projet depuis GitHub :

  1. Dans la ligne de commande, clonez le dépôt GitHub dans le répertoire flutter-codelabs :
git clone https://github.com/flutter/codelabs.git flutter-codelabs

Le répertoire flutter-codelabs contient le code d'une collection d'ateliers de programmation. Le code de cet atelier de programmation se trouve dans le répertoire flutter-codelabs/firebase-get-to-know-flutter. Le répertoire contient une série d'instantanés qui montrent à quoi votre projet devrait ressembler à la fin de chaque étape. Par exemple, vous êtes à la deuxième étape.

  1. Recherchez les fichiers correspondants pour la deuxième étape :
cd flutter-codelabs/firebase-get-to-know-flutter/step_02

Si vous souhaitez passer à la suite ou voir à quoi doit ressembler un élément après une étape, consultez le répertoire portant le nom de l'étape qui vous intéresse.

Importer l'application de démarrage

  • Ouvrez ou importez le répertoire flutter-codelabs/firebase-get-to-know-flutter/step_02 dans l'IDE de votre choix. Ce répertoire contient le code de départ de l'atelier de programmation, qui correspond à une application de meetup Flutter non fonctionnelle.

Localiser les fichiers à modifier

Le code de cette application est réparti dans plusieurs répertoires. Cette répartition des fonctionnalités facilite le travail, car elle regroupe le code par fonctionnalité.

  • Recherchez les fichiers suivants :
    • lib/main.dart : ce fichier contient le point d'entrée principal et le widget d'application.
    • lib/home_page.dart : ce fichier contient le widget de la page d'accueil.
    • lib/src/widgets.dart : ce fichier contient un certain nombre de widgets pour aider à standardiser le style de l'application. Ils composent l'écran de l'application de démarrage.
    • lib/src/authentication.dart : ce fichier contient une implémentation partielle de l'authentification avec un ensemble de widgets permettant de créer une expérience de connexion pour l'authentification Firebase basée sur l'adresse e-mail. Ces widgets pour le flux d'authentification ne sont pas encore utilisés dans l'application de démarrage, mais vous les ajouterez bientôt.

Ajoutez d'autres fichiers si nécessaire pour créer le reste de l'application.

Examiner le fichier lib/main.dart

Cette application utilise le package google_fonts pour définir Roboto comme police par défaut dans toute l'application. Vous pouvez explorer fonts.google.com et utiliser les polices que vous y trouverez dans différentes parties de l'application.

Vous utilisez les widgets d'assistance du fichier lib/src/widgets.dart sous la forme Header, Paragraph et IconAndDetail. Ces widgets éliminent le code en double pour réduire l'encombrement de la mise en page décrite dans HomePage. Cela permet également d'obtenir une apparence cohérente.

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

Écran d'accueil de l'application sur Android

Écran d'accueil de l'application sur iOS

Écran d'accueil de l'application sur le Web

Écran d'accueil de l'application sur macOS

3. Créer et configurer un projet Firebase

L'affichage des informations sur les événements est idéal pour vos invités, mais il n'est pas très utile en soi. Vous devez ajouter des fonctionnalités dynamiques à l'application. Pour ce faire, vous devez associer Firebase à votre application. Pour commencer à utiliser Firebase, vous devez créer et configurer un projet Firebase.

Créer un projet Firebase

  1. Connectez-vous à la console Firebase à l'aide de votre compte Google.
  2. Cliquez sur le bouton pour créer un projet, puis saisissez un nom de projet (par exemple, Firebase-Flutter-Codelab).
  3. Cliquez sur Continuer.
  4. Si vous y êtes invité, lisez et acceptez les Conditions d'utilisation de Firebase, puis cliquez sur Continuer.
  5. (Facultatif) Activez l'assistance IA dans la console Firebase (appelée "Gemini dans Firebase").
  6. Pour cet atelier de programmation, vous n'avez pas besoin de Google Analytics. Désactivez donc l'option Google Analytics.
  7. Cliquez sur Créer un projet, attendez que votre projet soit provisionné, puis cliquez sur Continuer.

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

Configurer les produits Firebase

L'application utilise les produits Firebase suivants, qui sont disponibles pour les applications Web :

  • Authentification : permet aux utilisateurs de se connecter à votre application.
  • Firestore : permet de sauvegarder des données structurées sur le cloud et d'être notifié instantanément en cas de modification des données.
  • Règles de sécurité Firebase : sécurisent votre base de données.

Certains de ces produits nécessitent une configuration particulière ou doivent être activés dans la console Firebase.

Activer l'authentification par connexion avec une adresse e-mail

  1. Dans le volet Vue d'ensemble du projet de la console Firebase, développez le menu Créer.
  2. Cliquez sur Authentification > Commencer > Méthode de connexion > E-mail/Mot de passe > Activer > Enregistrer.

58e3e3e23c2f16a4.png

Configurer Firestore

L'application Web utilise Firestore pour enregistrer des messages de chat et en recevoir.

Pour configurer Firestore dans votre projet Firebase :

  1. Dans le panneau de gauche de la console Firebase, développez Créer, puis sélectionnez Base de données Firestore.
  2. Cliquez sur Créer une base de données.
  3. Laissez le champ ID de la base de données défini sur (default).
  4. Sélectionnez un emplacement pour votre base de données, puis cliquez sur Suivant.
    Pour une application réelle, choisissez un emplacement proche de vos utilisateurs.
  5. Cliquez sur Démarrer en mode test. Lisez la clause de non-responsabilité concernant les règles de sécurité.
    Dans cet atelier de programmation, vous ajouterez des règles de sécurité pour protéger vos données. Ne distribuez ni n'exposez publiquement une application sans ajouter de règles de sécurité pour votre base de données.
  6. Cliquez sur Créer.

4. Configurer Firebase

Pour utiliser Firebase avec Flutter, vous devez effectuer les tâches suivantes afin de configurer le projet Flutter pour qu'il utilise correctement les bibliothèques FlutterFire :

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

Dans le répertoire de premier niveau de votre application Flutter, vous trouverez les sous-répertoires android, ios, macos et web, qui contiennent les fichiers de configuration propres aux plates-formes iOS et Android.

Configurer des dépendances

Vous devez ajouter les bibliothèques FlutterFire pour les deux produits Firebase que vous utilisez dans cette application : Authentication et Firestore.

  • Dans la ligne de commande, ajoutez les dépendances suivantes :
$ flutter pub add firebase_core

Le package firebase_core est le code commun requis pour tous les plug-ins Firebase Flutter.

$ flutter pub add firebase_auth

Le package firebase_auth permet l'intégration à l'authentification.

$ flutter pub add cloud_firestore

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

$ flutter pub add provider

Le package firebase_ui_auth fournit un ensemble de widgets et d'utilitaires pour accélérer le développement avec les flux d'authentification.

$ flutter pub add firebase_ui_auth

Vous avez ajouté les packages requis, mais vous devez également configurer les projets d'exécution iOS, Android, macOS et Web pour utiliser Firebase de manière appropriée. Vous utilisez également le package provider qui permet de séparer la logique métier de la logique d'affichage.

Installer la CLI FlutterFire

La CLI FlutterFire dépend de la CLI Firebase sous-jacente.

  1. Si vous ne l'avez pas déjà fait, installez la CLI Firebase sur votre machine.
  2. Installez la CLI FlutterFire :
$ dart pub global activate flutterfire_cli

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

Configurer vos applications

La CLI extrait des 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 guide à travers les processus suivants :

  1. Sélectionnez un projet Firebase en fonction du fichier .firebaserc ou dans la console Firebase.
  2. Déterminez les plates-formes pour la configuration, telles qu'Android, iOS, macOS et le Web.
  3. Identifiez les applications Firebase à partir desquelles extraire la configuration. Par défaut, la CLI tente de faire correspondre automatiquement les applications Firebase en fonction de la configuration de votre projet actuel.
  4. Générez un fichier firebase_options.dart dans votre projet.

Configurer macOS

Flutter sur macOS crée des applications entièrement mises en bac à sable. Étant donné que cette application s'intègre au réseau pour communiquer avec les serveurs Firebase, vous devez la configurer avec des droits d'accès au 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>

Pour en savoir plus, consultez la page Desktop support for Flutter (Compatibilité avec les applications de bureau pour Flutter).

5. Ajouter une fonctionnalité de confirmation de présence

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

Le projet que vous avez récupéré précédemment comprenait un ensemble de widgets qui implémentent l'interface utilisateur pour la majeure partie du flux d'authentification. Vous implémentez la logique métier pour intégrer l'authentification à l'application.

Ajouter la logique métier avec le package Provider

Utilisez le package provider pour rendre un objet d'état d'application centralisé disponible dans l'arborescence des widgets Flutter de l'application :

  1. Créez un fichier nommé app_state.dart avec le contenu suivant :

lib/app_state.dart

import 'package:firebase_auth/firebase_auth.dart'
    hide EmailAuthProvider, PhoneAuthProvider;
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_ui_auth/firebase_ui_auth.dart';
import 'package:flutter/material.dart';

import 'firebase_options.dart';

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

  bool _loggedIn = false;
  bool get loggedIn => _loggedIn;

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

    FirebaseUIAuth.configureProviders([
      EmailAuthProvider(),
    ]);

    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loggedIn = true;
      } else {
        _loggedIn = false;
      }
      notifyListeners();
    });
  }
}

Les instructions import présentent Firebase Core et Auth, extraient le package provider qui rend l'objet d'état de l'application disponible dans l'arborescence des widgets et incluent les widgets d'authentification du package firebase_ui_auth.

L'objet d'état de l'application ApplicationState a une responsabilité principale pour cette étape : alerter l'arborescence des widgets qu'un état authentifié a été mis à jour.

Vous n'utilisez un fournisseur que pour communiquer l'état de connexion d'un utilisateur à l'application. Pour permettre à un utilisateur de se connecter, vous utilisez les UI fournies par le package firebase_ui_auth, qui est un excellent moyen d'amorcer rapidement les écrans de connexion dans vos applications.

Intégrer le flux d'authentification

  1. Modifiez les importations en haut du fichier lib/main.dart :

lib/main.dart

import 'package:firebase_ui_auth/firebase_ui_auth.dart'; // new
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';               // new
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';                 // new

import 'app_state.dart';                                 // new
import 'home_page.dart';
  1. Connectez l'état de l'application à l'initialisation de l'application, puis ajoutez le flux d'authentification à HomePage :

lib/main.dart

void main() {
  // Modify from here...
  WidgetsFlutterBinding.ensureInitialized();

  runApp(ChangeNotifierProvider(
    create: (context) => ApplicationState(),
    builder: ((context, child) => const App()),
  ));
  // ...to here.
}

La modification apportée à la fonction main() permet au package de fournisseur d'être responsable de l'instanciation de l'objet d'état de l'application avec le widget ChangeNotifierProvider. Vous utilisez cette classe provider spécifique, car l'objet d'état de l'application étend la classe ChangeNotifier, ce qui permet au package provider de savoir quand réafficher les widgets dépendants.

  1. Mettez à jour votre application pour gérer la navigation vers les différents écrans que FirebaseUI vous fournit, en créant une configuration GoRouter :

lib/main.dart

// Add GoRouter configuration outside the App class
final _router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomePage(),
      routes: [
        GoRoute(
          path: 'sign-in',
          builder: (context, state) {
            return SignInScreen(
              actions: [
                ForgotPasswordAction(((context, email) {
                  final uri = Uri(
                    path: '/sign-in/forgot-password',
                    queryParameters: <String, String?>{
                      'email': email,
                    },
                  );
                  context.push(uri.toString());
                })),
                AuthStateChangeAction(((context, state) {
                  final user = switch (state) {
                    SignedIn state => state.user,
                    UserCreated state => state.credential.user,
                    _ => null
                  };
                  if (user == null) {
                    return;
                  }
                  if (state is UserCreated) {
                    user.updateDisplayName(user.email!.split('@')[0]);
                  }
                  if (!user.emailVerified) {
                    user.sendEmailVerification();
                    const snackBar = SnackBar(
                        content: Text(
                            'Please check your email to verify your email address'));
                    ScaffoldMessenger.of(context).showSnackBar(snackBar);
                  }
                  context.pushReplacement('/');
                })),
              ],
            );
          },
          routes: [
            GoRoute(
              path: 'forgot-password',
              builder: (context, state) {
                final arguments = state.uri.queryParameters;
                return ForgotPasswordScreen(
                  email: arguments['email'],
                  headerMaxExtent: 200,
                );
              },
            ),
          ],
        ),
        GoRoute(
          path: 'profile',
          builder: (context, state) {
            return ProfileScreen(
              providers: const [],
              actions: [
                SignedOutAction((context) {
                  context.pushReplacement('/');
                }),
              ],
            );
          },
        ),
      ],
    ),
  ],
);
// end of GoRouter configuration

// Change MaterialApp to MaterialApp.router and add the routerConfig
class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Firebase Meetup',
      theme: ThemeData(
        buttonTheme: Theme.of(context).buttonTheme.copyWith(
              highlightColor: Colors.deepPurple,
            ),
        primarySwatch: Colors.deepPurple,
        textTheme: GoogleFonts.robotoTextTheme(
          Theme.of(context).textTheme,
        ),
        visualDensity: VisualDensity.adaptivePlatformDensity,
        useMaterial3: true,
      ),
      routerConfig: _router, // new
    );
  }
}

Chaque écran est associé à un type d'action différent en fonction du nouvel état du flux d'authentification. Après la plupart des changements d'état lors de l'authentification, vous pouvez rediriger l'utilisateur vers un écran de votre choix, qu'il s'agisse de l'écran d'accueil ou d'un autre écran, comme celui du profil.

  1. Dans la méthode build de la classe HomePage, intégrez l'état de l'application au widget AuthFunc :

lib/home_page.dart

import 'package:firebase_auth/firebase_auth.dart' // new
    hide EmailAuthProvider, PhoneAuthProvider;    // new
import 'package:flutter/material.dart';           // new
import 'package:provider/provider.dart';          // new

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

class HomePage extends StatelessWidget {
  const HomePage({super.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, _) => AuthFunc(
                loggedIn: appState.loggedIn,
                signOut: () {
                  FirebaseAuth.instance.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 AuthFunc et l'encapsulez dans un widget Consumer. Le widget Consumer est la méthode habituelle pour utiliser le package provider afin de reconstruire une partie de l'arborescence lorsque l'état de l'application change. Le widget AuthFunc correspond aux widgets supplémentaires que vous testez.

Tester le flux d'authentification

cdf2d25e436bd48d.png

  1. Dans l'application, appuyez sur le bouton RSVP pour lancer la SignInScreen.

2a2cd6d69d172369.png

  1. Saisissez une adresse e-mail. Si vous êtes déjà inscrit, le système vous invite à saisir un mot de passe. Sinon, le système vous invite à remplir le formulaire d'inscription.

e5e65065dba36b54.png

  1. Saisissez un mot de passe de moins de six caractères pour vérifier le flux de gestion des erreurs. Si vous êtes inscrit, le mot de passe s'affiche à la place.
  2. Saisissez des mots de passe incorrects pour vérifier le flux de gestion des erreurs.
  3. Saisissez le mot de passe correct. Vous voyez l'expérience de connexion, qui permet à l'utilisateur de se déconnecter.

4ed811a25b0cf816.png

6. Écrire des messages dans Firestore

C'est bien de savoir que des utilisateurs arrivent, mais vous devez leur proposer autre chose à faire dans l'application. Et s'ils pouvaient laisser des messages dans un livre d'or ? Ils peuvent expliquer pourquoi ils ont hâte de venir ou qui ils espèrent rencontrer.

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

Modèle de données

Firestore est une base de données NoSQL. Les données qui y sont stockées sont divisées en collections, documents, champs et sous-collections. Vous stockez chaque message du chat en tant que document dans une collection guestbook, qui est une collection de premier niveau.

7c20dc8424bb1d84.png

Ajouter des messages à Firestore

Dans cette section, vous allez ajouter la fonctionnalité permettant aux utilisateurs d'écrire des messages dans la base de données. Vous commencez par ajouter un champ de formulaire et un bouton d'envoi, puis vous ajoutez le code qui relie ces éléments à la base de données.

  1. Créez un fichier nommé guest_book.dart, puis ajoutez un widget avec état GuestBook pour construire les éléments d'interface utilisateur d'un champ de message et d'un bouton d'envoi :

lib/guest_book.dart

import 'dart:async';

import 'package:flutter/material.dart';

import 'src/widgets.dart';

class GuestBook extends StatefulWidget {
  const GuestBook({required this.addMessage, super.key});

  final FutureOr<void> Function(String message) addMessage;

  @override
  State<GuestBook> 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'),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Deux points sont à noter ici. Tout d'abord, vous instanciez un formulaire pour pouvoir vérifier que le message contient bien du contenu et afficher un message d'erreur à l'utilisateur si ce n'est pas le cas. Pour valider un formulaire, vous accédez à l'état du formulaire derrière le formulaire avec un GlobalKey. Pour en savoir plus sur les clés et leur utilisation, consultez 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 un Row. Notez également que TextFormField est encapsulé dans un widget Expanded, ce qui oblige TextFormField à remplir tout espace supplémentaire dans la ligne. Pour mieux comprendre pourquoi cela est nécessaire, consultez 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.

  1. Modifiez le corps de HomePage pour ajouter les deux lignes suivantes à la fin 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 suffise à afficher le widget, cela ne suffit pas à faire quoi que ce soit d'utile. Vous allez bientôt mettre à jour ce code pour le rendre fonctionnel.

Aperçu de l'application

Écran d&#39;accueil de l&#39;application sur Android avec l&#39;intégration du chat

Écran d&#39;accueil de l&#39;application sur iOS avec l&#39;intégration du chat

Écran d&#39;accueil de l&#39;application sur le Web avec l&#39;intégration du chat

Écran d&#39;accueil de l&#39;application sur macOS avec l&#39;intégration du chat

Lorsqu'un utilisateur clique sur SEND (ENVOYER), l'extrait de code suivant est déclenché. Il ajoute le contenu du champ de saisie du message à la collection 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 dans la collection guestbook.

Notez que FirebaseAuth.instance.currentUser.uid fait référence à l'ID unique généré automatiquement que l'authentification fournit pour tous les utilisateurs connectés.

  • Dans le fichier lib/app_state.dart, ajoutez la méthode addMessageToGuestBook. Vous connecterez cette fonctionnalité à l'interface utilisateur à l'étape suivante.

lib/app_state.dart

import 'package:cloud_firestore/cloud_firestore.dart'; // new
import 'package:firebase_auth/firebase_auth.dart'
    hide EmailAuthProvider, PhoneAuthProvider;
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_ui_auth/firebase_ui_auth.dart';
import 'package:flutter/material.dart';

import 'firebase_options.dart';

class ApplicationState extends ChangeNotifier {

  // Current content of ApplicationState elided ...

  // Add from here...
  Future<DocumentReference> addMessageToGuestBook(String message) {
    if (!_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.
}

Connecter l'UI et la base de données

Vous disposez d'une UI dans laquelle l'utilisateur peut saisir le texte qu'il souhaite ajouter au livre d'or, ainsi que du code permettant d'ajouter l'entrée à Firestore. Il ne vous reste plus qu'à les associer.

  • Dans le fichier lib/home_page.dart, apportez la modification suivante au widget HomePage :

lib/home_page.dart

import 'package:firebase_auth/firebase_auth.dart'
    hide EmailAuthProvider, PhoneAuthProvider;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import 'app_state.dart';
import 'guest_book.dart';                         // new
import 'src/authentication.dart';
import 'src/widgets.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, _) => AuthFunc(
                loggedIn: appState.loggedIn,
                signOut: () {
                  FirebaseAuth.instance.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.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 affichez. Cela vous permet de réagir à un message saisi dans l'UI et de le publier dans la base de données. Dans la section suivante, vous allez vérifier si les messages ajoutés sont publiés dans la base de données.

Tester l'envoi de messages

  1. Si nécessaire, connectez-vous à l'application.
  2. Saisissez un message, tel que Hey there!, puis cliquez sur SEND (ENVOYER).

Cette action écrit le message dans votre base de données Firestore. Cependant, vous ne verrez pas le message dans votre application Flutter actuelle, car vous devez encore mettre en œuvre la récupération des données, ce que vous ferez à l'étape suivante. Toutefois, dans le tableau de bord Database de la console Firebase, vous pouvez voir le message que vous avez ajouté dans la collection guestbook. Si vous envoyez plus de messages, vous ajoutez plus de documents à votre collection guestbook. Par exemple, consultez l'extrait de code suivant :

713870af0b3b63c.png

7. Lire les messages

C'est super 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. Nous allons nous y atteler.

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'UI qui affiche les nouveaux messages. Vous ajoutez du code à l'état de l'application qui écoute les messages nouvellement ajoutés à l'application.

  1. Créez un fichier guest_book_message.dart et ajoutez la classe suivante pour exposer une vue structurée des données que vous stockez dans Firestore.

lib/guest_book_message.dart

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

  final String name;
  final String message;
}
  1. Dans le fichier lib/app_state.dart, ajoutez les importations suivantes :

lib/app_state.dart

import 'dart:async';                                     // new

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart'
    hide EmailAuthProvider, PhoneAuthProvider;
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_ui_auth/firebase_ui_auth.dart';
import 'package:flutter/material.dart';

import 'firebase_options.dart';
import 'guest_book_message.dart';                        // new
  1. Dans la section ApplicationState où vous définissez l'état et les getters, ajoutez les lignes suivantes :

lib/app_state.dart

  bool _loggedIn = false;
  bool get loggedIn => _loggedIn;

  // Add from here...
  StreamSubscription<QuerySnapshot>? _guestBookSubscription;
  List<GuestBookMessage> _guestBookMessages = [];
  List<GuestBookMessage> get guestBookMessages => _guestBookMessages;
  // ...to here.
  1. Dans la section d'initialisation de ApplicationState, ajoutez les lignes suivantes pour vous abonner à une requête sur la collection de documents lorsqu'un utilisateur se connecte et se désabonne lorsqu'il se déconnecte :

lib/app_state.dart

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

    FirebaseUIAuth.configureProviders([
      EmailAuthProvider(),
    ]);
    
    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loggedIn = true;
        _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();
        });
      } else {
        _loggedIn = false;
        _guestBookMessages = [];
        _guestBookSubscription?.cancel();
      }
      notifyListeners();
    });
  }

Cette section est importante, car c'est là que vous construisez une requête sur la collection guestbook et que vous gérez l'abonnement et la désinscription à cette collection. Vous écoutez le flux, où vous reconstruisez un cache local des messages de la collection guestbook et stockez également une référence à cet abonnement afin de pouvoir vous en désabonner ultérieurement. Il se passe beaucoup de choses ici. Vous devriez donc l'explorer dans un débogueur pour inspecter ce qui se passe et obtenir un modèle mental plus clair. Pour en savoir plus, consultez Obtenir des mises à jour en temps réel avec Firestore.

  1. Dans le fichier lib/guest_book.dart, ajoutez l'importation suivante :
import 'guest_book_message.dart';
  1. Dans le widget GuestBook, ajoutez une liste de messages à la configuration pour connecter cet état changeant à l'interface utilisateur :

lib/guest_book.dart

class GuestBook extends StatefulWidget {
  // Modify the following line:
  const GuestBook({
    super.key, 
    required this.addMessage, 
    required this.messages,
  });

  final FutureOr<void> Function(String message) addMessage;
  final List<GuestBookMessage> messages; // new

  @override
  _GuestBookState createState() => _GuestBookState();
}
  1. Dans _GuestBookState, modifiez la méthode build comme suit pour exposer cette configuration :

lib/guest_book.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 vous ajoutez une collection for à la fin des enfants de Column pour générer un nouveau Paragraph pour chaque message de la liste des messages.

  1. Mettez à jour le corps de HomePage pour construire correctement GuestBook avec le nouveau paramètre messages :

lib/home_page.dart

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

Tester la synchronisation des messages

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

Tester la synchronisation des messages :

  1. Dans l'application, recherchez les messages que vous avez créés précédemment dans la base de données.
  2. rédiger de nouveaux messages ; Elles s'affichent instantanément.
  3. Ouvrez votre espace de travail dans plusieurs fenêtres ou onglets. Les messages sont synchronisés en temps réel dans les fenêtres et les onglets.
  4. Facultatif : Dans le menu Database (Base de données) de la console Firebase, supprimez, modifiez ou ajoutez manuellement des messages. Toutes les modifications apparaissent dans l'UI.

Félicitations ! Vous avez lu des documents Firestore dans votre application !

Aperçu de l'application

Écran d&#39;accueil de l&#39;application sur Android avec l&#39;intégration du chat

Écran d&#39;accueil de l&#39;application sur iOS avec l&#39;intégration du chat

Écran d&#39;accueil de l&#39;application sur le Web avec l&#39;intégration du chat

Écran d&#39;accueil de l&#39;application sur macOS avec l&#39;intégration du chat

8. Configurer des règles de sécurité de base

Vous avez initialement configuré Firestore pour utiliser le mode test, ce qui signifie que votre base de données est ouverte en lecture et en écriture. Toutefois, vous ne devez utiliser le mode test que lors des premières étapes du développement. Nous vous recommandons de configurer des règles de sécurité pour votre base de données au fur et à mesure du développement de votre application. La sécurité fait partie intégrante de la structure et du comportement de votre application.

Les règles de sécurité Firebase 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 à toutes sortes d'éléments : toutes les écritures de l'intégralité de la base de données, ou bien les opérations sur un document spécifique.

Configurez des règles de sécurité de base :

  1. Dans le menu Développer de la console Firebase, cliquez sur Base de données > Règles. Les règles de sécurité par défaut suivantes devraient s'afficher, ainsi qu'un avertissement indiquant qu'elles sont publiques :

7767a2d2e64e7275.png

  1. 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.
  }
}

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

  1. Ajoutez les règles de lecture et d'écriture à votre ensemble de règles :
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, seuls les utilisateurs connectés peuvent lire les messages du livre d'or, mais seul l'auteur d'un message peut le modifier.

  1. 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 : Mettez en pratique ce que vous avez appris

Enregistrer l'état de la réponse d'un participant

Pour le moment, votre application ne permet aux utilisateurs de discuter que lorsqu'ils sont intéressés par l'événement. De plus, la seule façon de savoir si quelqu'un arrive est de le demander dans le chat.

À cette étape, vous vous organisez et vous indiquez le nombre de personnes qui viendront. Vous ajoutez quelques fonctionnalités à l'état de l'application. La première est la possibilité pour un utilisateur connecté d'indiquer s'il participe ou non. Le second est un compteur indiquant le nombre de participants.

  1. Dans le fichier lib/app_state.dart, ajoutez les lignes suivantes à la section des accesseurs de ApplicationState afin que le code de l'UI puisse interagir avec cet état :

lib/app_state.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});
  }
}
  1. Mettez à jour la méthode init() de ApplicationState comme suit :

lib/app_state.dart

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

    FirebaseUIAuth.configureProviders([
      EmailAuthProvider(),
    ]);

    // 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) {
        _loggedIn = true;
        _emailVerified = user.emailVerified;
        _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 {
        _loggedIn = false;
        _emailVerified = false;
        _guestBookMessages = [];
        _guestBookSubscription?.cancel();
        _attendingSubscription?.cancel(); // new
      }
      notifyListeners();
    });
  }

Ce code ajoute une requête toujours abonnée pour déterminer le nombre de participants et une deuxième requête qui n'est active que lorsqu'un utilisateur est connecté pour déterminer s'il participe.

  1. Ajoutez l'énumération suivante en haut du fichier lib/app_state.dart.

lib/app_state.dart

enum Attending { yes, no, unknown }
  1. Créez un fichier yes_no_selection.dart et définissez un widget qui se comporte comme des boutons radio :

lib/yes_no_selection.dart

import 'package:flutter/material.dart';

import 'app_state.dart';
import 'src/widgets.dart';

class YesNoSelection extends StatelessWidget {
  const YesNoSelection(
      {super.key, 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: [
              FilledButton(
                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),
              FilledButton(
                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'),
              ),
            ],
          ),
        );
    }
  }
}

Il commence dans un état indéterminé, sans que Oui ni Non ne soient sélectionnés. Une fois que l'utilisateur a sélectionné sa réponse, vous mettez en évidence l'option choisie avec un bouton plein et l'autre option est affichée avec un rendu plat.

  1. Mettez à jour la méthode build() de HomePage pour profiter de YesNoSelection, permettre à un utilisateur connecté d'indiquer s'il participe à l'événement et afficher le nombre de participants :

lib/home_page.dart

Consumer<ApplicationState>(
  builder: (context, appState, _) => Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      // Add from here...
      switch (appState.attendees) {
        1 => const Paragraph('1 person going'),
        >= 2 => Paragraph('${appState.attendees} people going'),
        _ => const Paragraph('No one going'),
      },
      // ...to here.
      if (appState.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

Vous avez déjà configuré des règles. Les données que vous ajouterez à l'aide des boutons seront donc refusées. Vous devez mettre à jour les règles pour autoriser les ajouts à la collection attendees.

  1. Dans la collection attendees, récupérez l'UID d'authentification que vous avez utilisé comme nom de document et vérifiez que le uid de l'auteur est le même que celui du document qu'il rédige :
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // ... //
    match /attendees/{userId} {
      allow read: if true;
      allow write: if request.auth.uid == userId;
    }
  }
}

Cela permet à tout le monde de lire la liste des participants, car elle ne contient aucune donnée privée, mais seul l'organisateur peut la modifier.

  1. 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;

    }
  }
}
  1. Facultatif : Dans l'application, cliquez sur les boutons pour afficher les résultats dans le tableau de bord Firestore de la console Firebase.

Aperçu de l'application

Écran d&#39;accueil de l&#39;application sur Android

Écran d&#39;accueil de l&#39;application sur iOS

Écran d&#39;accueil de l&#39;application sur le Web

Écran d&#39;accueil de l&#39;application sur macOS

10. Félicitations !

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

En savoir plus