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
- Connaissance de Flutter
- Le SDK Flutter
- Un éditeur de texte de votre choix
Ce que vous apprendrez
- Comment créer un événement RSVP et une application de chat de livre d'or sur Android, iOS, le Web et macOS avec Flutter.
- Comment authentifier les utilisateurs avec Firebase Authentication et synchroniser les données avec Firestore.
Ce dont vous aurez besoin
L'un des appareils suivants :
- Un appareil Android ou iOS physique connecté à votre ordinateur et défini en mode développeur.
- Le simulateur iOS (nécessite les outils Xcode ).
- L'émulateur Android (nécessite une configuration dans Android Studio ).
Vous avez également besoin 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 plugins Dart et Flutter, comme Android Studio ou Visual Studio Code .
- La dernière version
stable
de Flutter oubeta
si vous aimez vivre à la limite. - Un compte Google pour la création et la gestion de votre projet Firebase.
- La CLI
Firebase
s'est connectée à votre compte Google.
2. Obtenez l'exemple de code
Téléchargez la version initiale de votre projet depuis GitHub :
- Depuis 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 de codelabs. Le code de ce laboratoire de programmation se trouve dans le 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 sur la deuxième étape.
- Recherchez les fichiers correspondants pour la deuxième étape :
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 qui ont besoin de travail
Le code de cette application est réparti sur plusieurs répertoires. Cette répartition des fonctionnalités facilite le travail car elle regroupe le code par fonctionnalité.
- Localisez les fichiers suivants :
-
lib/main.dart
: Ce fichier contient le point d'entrée principal et le widget de l'application. -
lib/home_page.dart
: Ce fichier contient le widget de la page d'accueil. -
lib/src/widgets.dart
: Ce fichier contient une poignée de widgets pour aider à normaliser 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 pour créer une expérience utilisateur de connexion pour l'authentification par e-mail Firebase. 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.
-
Vous ajoutez des fichiers supplémentaires si nécessaire pour créer le reste de l'application.
Examinez le fichier lib/main.dart
Cette application tire parti du package google_fonts
pour faire de Roboto la police par défaut dans toute l'application. Vous pouvez 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 du fichier lib/src/widgets.dart
sous la forme de Header
, Paragraph
et IconAndDetail
. Ces widgets éliminent le code dupliqué pour réduire l'encombrement de la mise en page décrite dans HomePage
. Cela permet également une apparence et une sensation cohérentes.
Voici à quoi ressemble votre application sur Android, iOS, le Web et macOS :
3. Créer et configurer un projet Firebase
L'affichage des informations sur l'événement est idéal pour vos invités, mais il n'est pas très utile pour quiconque seul. Vous devez ajouter des fonctionnalités dynamiques à l'application. Pour ce faire, vous devez connecter Firebase à votre application. Pour démarrer avec Firebase, vous devez créer et configurer un projet Firebase.
Créer un projet Firebase
- Connectez-vous à Firebase .
- Dans la console, cliquez sur Ajouter un projet ou Créer un projet .
- Dans le champ Nom du projet , saisissez Firebase-Flutter-Codelab , puis cliquez sur Continuer .
- Cliquez sur les options de création de projet. Si vous y êtes invité, acceptez les conditions de Firebase, mais ignorez la configuration de Google Analytics, car vous ne l'utiliserez pas pour cette application.
Pour en savoir plus sur les projets Firebase, consultez Comprendre les projets 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 : enregistre les données structurées sur le cloud et reçoit des notifications instantanées lorsque les données changent.
- Règles de sécurité Firebase : sécurise votre base de données.
Certains de ces produits nécessitent une configuration spéciale ou vous devez les activer dans la console Firebase.
Activer l'authentification de connexion par e-mail
- Dans le volet de présentation du projet de la console Firebase, développez le menu " Build ".
- Cliquez sur Authentification > Démarrer > Méthode de connexion > E-mail/Mot de passe > Activer > Enregistrer .
Activer Firestore
L'application Web utilise Firestore pour enregistrer les messages de chat et recevoir de nouveaux messages de chat.
Activer Firestore :
- Dans le menu Construire , cliquez sur Cloud Firestore > Créer une base de données .
- Sélectionnez Démarrer en mode test, puis lisez la clause de non-responsabilité concernant 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 , puis sélectionnez l'emplacement de votre base de données. Vous pouvez utiliser la valeur par défaut. Vous ne pouvez pas modifier l'emplacement ultérieurement.
- Cliquez sur Activer .
4. Configurez Firebase
Pour utiliser Firebase avec Flutter, vous devez effectuer les tâches suivantes pour configurer le projet Flutter afin qu'il utilise 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, puis ajoutez-le au code.
Dans le répertoire de niveau supérieur de votre application Flutter, il existe des sous-répertoires android
, ios
, macos
et web
, qui contiennent respectivement les fichiers de configuration spécifiques à la plate-forme pour iOS et Android.
Configurer les dépendances
Vous devez ajouter les bibliothèques FlutterFire
pour les deux produits Firebase que vous utilisez dans cette application : Authentication et Firestore.
- À partir de 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 plugins Firebase Flutter.
$ flutter pub add firebase_auth
Le package firebase_auth
permet l'intégration avec 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 augmenter la vitesse des développeurs avec les flux d'authentification.
$ flutter pub add firebase_ui_auth
Vous avez ajouté les packages requis, mais vous devez également configurer les projets iOS, Android, macOS et Web Runner pour utiliser correctement Firebase. Vous utilisez également le package de 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.
- Si vous ne l'avez pas déjà fait, installez la CLI Firebase sur votre machine.
- Installez la CLI FlutterFire :
$ dart pub global activate flutterfire_cli
Une fois installée, la commande flutterfire
est disponible dans le monde entier.
Configurez 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 guide à travers les processus suivants :
- Sélectionnez un projet Firebase basé sur le fichier
.firebaserc
ou depuis la console Firebase. - Déterminez les plates-formes de configuration, telles qu'Android, iOS, macOS et Web.
- 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 actuelle de votre projet.
- 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 au réseau pour communiquer avec les serveurs Firebase, vous devez 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>
Pour plus d'informations, voir Prise en charge de bureau pour Flutter .
5. Ajouter la fonctionnalité RSVP
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 Web, il existe des packages FirebaseUI Auth
, mais vous devez créer cette capacité pour Flutter.
Le projet que vous avez récupéré précédemment comprenait un ensemble de widgets qui implémente 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 une logique métier avec le package Provider
Utilisez le package de provider
pour rendre un objet d'état d'application centralisé disponible dans l'arborescence des widgets Flutter de l'application :
- Créez un nouveau 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
introduisent Firebase Core et Auth, extraient le package du 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
.
Cet objet d'état d'application ApplicationState
a une responsabilité principale pour cette étape, qui est d'alerter l'arborescence du widget qu'il y a eu une mise à jour vers un état authentifié.
Vous utilisez uniquement un fournisseur pour communiquer l'état du statut de connexion d'un utilisateur à l'application. Pour permettre à un utilisateur de se connecter, vous utilisez les interfaces utilisateur fournies par le package firebase_ui_auth
, qui est un excellent moyen de démarrer rapidement les écrans de connexion dans vos applications.
Intégrer le flux d'authentification
- 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';
- 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 de la fonction main()
rend le package du fournisseur responsable de l'instanciation de l'objet d'état de l'application avec le widget ChangeNotifierProvider
. Vous utilisez cette classe de provider
spécifique car l'objet d'état de l'application étend la classe ChangeNotifier
, qui permet au package de provider
de savoir quand réafficher les widgets dépendants.
- Mettez à jour votre application pour gérer la navigation vers les différents écrans fournis par FirebaseUI, 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) {
if (state is SignedIn || state is UserCreated) {
var user = (state is SignedIn)
? state.user
: (state as UserCreated).credential.user;
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.queryParams;
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 dans l'authentification, vous pouvez rediriger vers un écran préféré, qu'il s'agisse de l'écran d'accueil ou d'un écran différent, tel que le profil.
- Dans la méthode build de la classe
HomePage
, intégrez l'état de l'application au widgetAuthFunc
:
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 Consommateur est le moyen habituel d'utiliser le package du provider
pour reconstruire une partie de l'arborescence lorsque l'état de l'application change. Le widget AuthFunc
est le widget supplémentaire que vous testez.
Tester le flux d'authentification
- Dans l'application, appuyez sur le bouton RSVP pour lancer le
SignInScreen
.
- Entrez une adresse email. Si vous êtes déjà inscrit, le système vous invite à entrer un mot de passe. Sinon, le système vous invite à remplir le formulaire d'inscription.
- Entrez un mot de passe de moins de six caractères pour vérifier le flux de traitement des erreurs. Si vous êtes inscrit, vous voyez le mot de passe pour à la place.
- Entrez des mots de passe incorrects pour vérifier le flux de gestion des erreurs.
- Entrez le mot de passe correct. Vous voyez l'expérience de connexion, qui offre à l'utilisateur la possibilité de se déconnecter.
6. Écrivez des messages sur Firestore
C'est bien de savoir que les utilisateurs viennent, mais vous devez donner 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 utilisez Firestore .
Modèle de données
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 stockez chaque message du chat en tant que document dans une collection de gustbook
d'or, qui est une collection de niveau supérieur.
Ajouter des messages à Firestore
Dans cette section, vous ajoutez la fonctionnalité permettant aux utilisateurs d'écrire des messages dans la base de données. Tout d'abord, vous ajoutez un champ de formulaire et un bouton d'envoi, puis vous ajoutez le code qui relie ces éléments à la base de données.
- Créez un nouveau fichier nommé
guest_book.dart
, ajoutez un widget avec étatGuestBook
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'),
],
),
),
],
),
),
);
}
}
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 contient réellement du contenu et montrer à l'utilisateur un message d'erreur s'il n'y en a pas. Pour valider un formulaire, vous accédez à l'état du formulaire derrière le formulaire avec un GlobalKey
. Pour plus d'informations 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 enveloppé dans un widget Expanded
, ce qui force TextFormField
à remplir tout espace supplémentaire dans la ligne. Pour mieux comprendre pourquoi cela est nécessaire, voir 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.
- Modifiez le corps de
HomePage
pour ajouter les deux lignes suivantes à la fin des enfants deListView
:
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 mettez à jour ce code sous peu pour le rendre fonctionnel.
Aperçu de l'application
Lorsqu'un utilisateur clique sur SEND , il déclenche l'extrait de code suivant. 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 dans 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 l'authentification donne à tous les utilisateurs connectés.
- Dans le fichier
lib/app_state.dart
, ajoutez la méthodeaddMessageToGuestBook
. Vous connectez 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'interface utilisateur et 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 à Firestore. Maintenant, tout ce que vous avez à faire est de connecter les deux.
- Dans le fichier
lib/home_page.dart
, apportez la modification suivante au widgetHomePage
:
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 à quelqu'un qui saisit un message dans l'interface utilisateur et de le publier dans la base de données. Dans la section suivante, vous testez si les messages ajoutés sont publiés dans la base de données.
Tester l'envoi de messages
- Si nécessaire, connectez-vous à l'application.
- Entrez un message, tel que
Hey there!
, puis cliquez sur ENVOYER .
Cette action écrit le message dans votre base de données Firestore. Cependant, vous ne voyez pas le message dans votre application Flutter réelle, car vous devez toujours implémenter la récupération des données, ce que vous ferez à l'étape suivante. Cependant, dans le tableau de bord de la base de données de la console Firebase, vous pouvez voir votre message ajouté dans la collection du guestbook
d'or. Si vous envoyez plus de messages, vous ajoutez plus de documents à votre collection de livres d' guestbook
. Par exemple, consultez l'extrait de code suivant :
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. Il est temps d'arranger ç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 ajoutez du code à l'état de l'application qui écoute les nouveaux messages ajoutés à partir de l'application.
- Créez un nouveau fichier
guest_book_message.dart
, 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;
}
- 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
- Dans la section d'
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.
- 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ésabonner 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 de livres d' guestbook
et 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, vous devriez donc l'explorer dans un débogueur pour inspecter ce qui se passe pour obtenir un modèle mental plus clair. Pour plus d'informations, consultez Obtenir des mises à jour en temps réel avec Firestore .
- Dans le fichier
lib/guest_book.dart
, ajoutez l'import suivant :
import 'guest_book_message.dart';
- Dans le widget
GuestBook
, ajoutez une liste de messages dans le cadre de 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();
}
- Dans
_GuestBookState
, modifiez la méthode debuild
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.
- Mettez à jour le corps de
HomePage
pour construire correctementGuestBook
avec le nouveau paramètremessages
:
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 :
- Dans l'application, recherchez les messages que vous avez créés précédemment dans la base de données.
- Rédigez de nouveaux messages. Ils apparaissent instantanément.
- Ouvrez votre espace de travail dans plusieurs fenêtres ou onglets. Les messages se synchronisent en temps réel sur les fenêtres et les onglets.
- Facultatif : dans le menu Base de données de la console Firebase, supprimez, modifiez ou ajoutez manuellement de nouveaux messages. Toutes les modifications apparaissent dans l'interface utilisateur.
Toutes nos félicitations! Vous lisez les documents Firestore dans votre application !
Aperçu de l'application
8. Mettre en place 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. Cependant, vous ne devez utiliser le mode test qu'au cours des 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é 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 à n'importe quoi, de toutes les écritures dans l'ensemble de la base de données aux opérations sur un document spécifique.
Configurez des règles de sécurité de base :
- Dans le menu Développer de la console Firebase, cliquez sur Base de données > Règles . Vous devriez voir les règles de sécurité par défaut suivantes et un avertissement indiquant que les règles sont publiques :
- 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 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 :
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 dans le livre d'or, mais seul l'auteur d'un message peut modifier un message.
- 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
À l'heure actuelle, votre application n'autorise les personnes à discuter que lorsqu'elles sont intéressées par l'événement. De plus, la seule façon de savoir si quelqu'un vient est lorsqu'il le dit dans le chat.
Dans cette étape, vous vous organisez et faites savoir aux gens combien de personnes viennent. Vous ajoutez quelques fonctionnalités à l'état de l'application. La première est la possibilité pour un utilisateur connecté de désigner s'il participe. Le second est un compteur du nombre de personnes présentes.
- Dans le fichier
lib/app_state.dart
, ajoutez les lignes suivantes à la section accessors deApplicationState
afin que le code de l'interface utilisateur 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});
}
}
- Mettez à jour la méthode
init()
deApplicationState
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) {
_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 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 si l'utilisateur participe.
- Ajoutez l'énumération suivante en haut du fichier
lib/app_state.dart
.
lib/app_state.dart
enum Attending { yes, no, unknown }
- Créez un nouveau fichier
yes_no_selection.dart
, définissez un nouveau widget qui agit 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: [
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'),
),
],
),
);
}
}
}
Il démarre dans un état indéterminé avec ni Oui ni Non sélectionnés. Une fois que l'utilisateur a sélectionné s'il participe, vous affichez cette option en surbrillance avec un bouton plein et l'autre option s'efface avec un rendu plat.
- Mettez à jour la méthode
build()
deHomePage
pour tirer parti deYesNoSelection
, autorisez un utilisateur connecté à désigner s'il participe et affichez le nombre de participants à l'événement :
lib/home_page.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.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é certaines règles, donc les données que vous ajoutez avec les boutons seront rejetées. Vous devez mettre à jour les règles pour autoriser les ajouts à la collection de attendees
.
- Dans la collection des
attendees
, saisissez l'UID d'authentification que vous avez utilisé comme nom de document et vérifiez que l'uid
de l'émetteur 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 il n'y a pas de données privées, mais seul le créateur peut la mettre à jour.
- 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 : 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
10. Félicitations !
Vous avez utilisé Firebase pour créer une application Web interactive en temps réel !
Apprendre encore plus
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
- Connaissance de Flutter
- Le SDK Flutter
- Un éditeur de texte de votre choix
Ce que vous apprendrez
- Comment créer un événement RSVP et une application de chat de livre d'or sur Android, iOS, le Web et macOS avec Flutter.
- Comment authentifier les utilisateurs avec Firebase Authentication et synchroniser les données avec Firestore.
Ce dont vous aurez besoin
L'un des appareils suivants :
- Un appareil Android ou iOS physique connecté à votre ordinateur et défini en mode développeur.
- Le simulateur iOS (nécessite les outils Xcode ).
- L'émulateur Android (nécessite une configuration dans Android Studio ).
Vous avez également besoin 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 plugins Dart et Flutter, comme Android Studio ou Visual Studio Code .
- La dernière version
stable
de Flutter oubeta
si vous aimez vivre à la limite. - Un compte Google pour la création et la gestion de votre projet Firebase.
- La CLI
Firebase
s'est connectée à votre compte Google.
2. Obtenez l'exemple de code
Téléchargez la version initiale de votre projet depuis GitHub :
- Depuis 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 de codelabs. Le code de ce laboratoire de programmation se trouve dans le 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 sur la deuxième étape.
- Recherchez les fichiers correspondants pour la deuxième étape :
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 qui ont besoin de travail
Le code de cette application est réparti sur plusieurs répertoires. Cette répartition des fonctionnalités facilite le travail car elle regroupe le code par fonctionnalité.
- Localisez les fichiers suivants :
-
lib/main.dart
: Ce fichier contient le point d'entrée principal et le widget de l'application. -
lib/home_page.dart
: Ce fichier contient le widget de la page d'accueil. -
lib/src/widgets.dart
: Ce fichier contient une poignée de widgets pour aider à normaliser 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 pour créer une expérience utilisateur de connexion pour l'authentification par e-mail Firebase. 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.
-
Vous ajoutez des fichiers supplémentaires si nécessaire pour créer le reste de l'application.
Examinez le fichier lib/main.dart
Cette application tire parti du package google_fonts
pour faire de Roboto la police par défaut dans toute l'application. Vous pouvez 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 du fichier lib/src/widgets.dart
sous la forme de Header
, Paragraph
et IconAndDetail
. Ces widgets éliminent le code dupliqué pour réduire l'encombrement de la mise en page décrite dans HomePage
. Cela permet également une apparence et une sensation cohérentes.
Voici à quoi ressemble votre application sur Android, iOS, le Web et macOS :
3. Créer et configurer un projet Firebase
L'affichage des informations sur l'événement est idéal pour vos invités, mais il n'est pas très utile pour quiconque seul. Vous devez ajouter des fonctionnalités dynamiques à l'application. Pour ce faire, vous devez connecter Firebase à votre application. Pour démarrer avec Firebase, vous devez créer et configurer un projet Firebase.
Créer un projet Firebase
- Connectez-vous à Firebase .
- Dans la console, cliquez sur Ajouter un projet ou Créer un projet .
- Dans le champ Nom du projet , saisissez Firebase-Flutter-Codelab , puis cliquez sur Continuer .
- Cliquez sur les options de création de projet. Si vous y êtes invité, acceptez les conditions de Firebase, mais ignorez la configuration de Google Analytics, car vous ne l'utiliserez pas pour cette application.
Pour en savoir plus sur les projets Firebase, consultez Comprendre les projets 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 : enregistre les données structurées sur le cloud et reçoit des notifications instantanées lorsque les données changent.
- Règles de sécurité Firebase : sécurise votre base de données.
Certains de ces produits nécessitent une configuration spéciale ou vous devez les activer dans la console Firebase.
Activer l'authentification de connexion par e-mail
- Dans le volet de présentation du projet de la console Firebase, développez le menu " Build ".
- Cliquez sur Authentification > Démarrer > Méthode de connexion > E-mail/Mot de passe > Activer > Enregistrer .
Activer Firestore
L'application Web utilise Firestore pour enregistrer les messages de chat et recevoir de nouveaux messages de chat.
Activer Firestore :
- Dans le menu Construire , cliquez sur Cloud Firestore > Créer une base de données .
- Sélectionnez Démarrer en mode test, puis lisez la clause de non-responsabilité concernant 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 , puis sélectionnez l'emplacement de votre base de données. Vous pouvez utiliser la valeur par défaut. Vous ne pouvez pas modifier l'emplacement ultérieurement.
- Cliquez sur Activer .
4. Configurez Firebase
Pour utiliser Firebase avec Flutter, vous devez effectuer les tâches suivantes pour configurer le projet Flutter afin qu'il utilise 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, puis ajoutez-le au code.
In the top-level directory of your Flutter app, there are android
, ios
, macos
and web
subdirectories, which 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 that you use in this app: Authentication and Firestore.
- From the command line, add the following depencies:
$ flutter pub add firebase_core
The firebase_core
package is the common code required for all Firebase Flutter plugins.
$ flutter pub add firebase_auth
The firebase_auth
package enables integration with Authentication.
$ flutter pub add cloud_firestore
The cloud_firestore
package enables access to Firestore data storage.
$ flutter pub add provider
The firebase_ui_auth
package provides a set of widgets and utilities to increase developer velocity with authentication flows.
$ flutter pub add firebase_ui_auth
You added the required packages, but you also need to configure the iOS, Android, macOS, and Web runner projects to appropriately use Firebase. You also use the provider
package that enables separation of business logic from display logic.
Install the FlutterFire CLI
The FlutterFire CLI depends on the underlying Firebase CLI.
- If you haven't done so already, install the Firebase CLI on your machine.
- Install the FlutterFire CLI:
$ dart pub global activate flutterfire_cli
Once installed, the flutterfire
command is globally available.
Configure your apps
The CLI extracts information from your Firebase project and selected project apps to generate all the configuration for a specific platform.
In the root of your app, run the configure
command:
$ flutterfire configure
The configuration command guides you through the following processes:
- Select a Firebase project based on the
.firebaserc
file or from the Firebase Console. - Determine platforms for configuration, such as Android, iOS, macOS, and web.
- Identify the Firebase apps from which to extract configuration. By default, the CLI attempts to automatically match Firebase apps based on your current project configuration.
- Generate a
firebase_options.dart
file in your project.
Configure macOS
Flutter on macOS builds fully sandboxed apps. As this app integrates with the network to communicate with the Firebase servers, you need to configure your app 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>
For more information, see Desktop support for Flutter .
5. Add RSVP functionality
Now that you added Firebase to the app, you can create an RSVP button that registers people with Authentication . For Android native, iOS native, and Web, there are prebuilt FirebaseUI Auth
packages, but you need to build this capability for Flutter.
The project that you retrieved earlier included a set of widgets that implements the user interface for most of the authentication flow. You implement the business logic to integrate Authentication with the app.
Add business logic with the Provider
package
Use the provider
package to make a centralized app state object available throughout the app's tree of Flutter widgets:
- Create a new file named
app_state.dart
with the following content:
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();
});
}
}
The import
statements introduce Firebase Core and Auth, pull in the provider
package that makes app state object available throughout the widget tree, and include the authentication widgets from the firebase_ui_auth
package.
This ApplicationState
application state object has one main responsibility for this step, which is to alert the widget tree that there was an update to an authenticated state.
You only use a provider to communicate the state of a user's login status to the app. To let a user log in, you use the UIs provided by the firebase_ui_auth
package, which is a great way to quickly bootstrap login screens in your apps.
Integrate the authentication flow
- Modify the imports at the top of the
lib/main.dart
file:
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';
- Connect the app state with the app initialization and then add the authentication flow to
HomePage
:
lib/main.dart
void main() {
// Modify from here...
WidgetsFlutterBinding.ensureInitialized();
runApp(ChangeNotifierProvider(
create: (context) => ApplicationState(),
builder: ((context, child) => const App()),
));
// ...to here.
}
The modification to the main()
function makes the provider package responsible for the instantiation of the app state object with the ChangeNotifierProvider
widget. You use this specific provider
class because the app state object extends the ChangeNotifier
class, which lets the provider
package know when to redisplay dependent widgets.
- Update your app to handle navigation to different screens that FirebaseUI provides for you, by creating a
GoRouter
configuration:
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) {
if (state is SignedIn || state is UserCreated) {
var user = (state is SignedIn)
? state.user
: (state as UserCreated).credential.user;
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.queryParams;
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
);
}
}
Each screen has a different type of action associated with it based on the new state of the authentication flow. After most state changes in authentication, you can reroute back to a preferred screen, whether it's the home screen or a different screen, such as profile.
- In the
HomePage
class's build method, integrate the app state with theAuthFunc
widget:
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!',
),
],
),
);
}
}
You instantiate the AuthFunc
widget and wrap it in a Consumer
widget. The Consumer widget is the usual way that the provider
package can be used to rebuild part of the tree when the app state changes. The AuthFunc
widget is the supplementary widgets that you test.
Test the authentication flow
- In the app, tap the RSVP button to initiate the
SignInScreen
.
- Enter an email address. If you're already registered, the system prompts you to enter a password. Otherwise, the system prompts you to complete the registration form.
- Enter a password that's less than six characters to check the error-handling flow. If you're registered, you see the password for instead.
- Enter incorrect passwords to check the error-handling flow.
- Enter the correct password. You see the logged-in experience, which offers the user the ability to log out.
6. Write messages to Firestore
It's great to know that users are coming, but you need to 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 use Firestore .
Data model
Firestore is a NoSQL database, and data stored in the database is split into collections, documents, fields, and subcollections. You store each message of the chat as a document in a gustbook
collection, which is a top-level collection.
Add messages to Firestore
In this section, you add the functionality for users to write messages to the database. First, you add a form field and send button, and then you add the code that connects these elements with the database.
- Create a new file named
guest_book.dart
, add aGuestBook
stateful widget to construct the UI elements of a message field and a send button:
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'),
],
),
),
],
),
),
);
}
}
There are a couple of points of interest here. First, you instantiate a form so that you can validate that the message actually contains content and show the user an error message if there isn't any. To validate a form, you access the form state behind the form with a GlobalKey
. For more information about Keys and how to use them, see When to Use Keys .
Also note the way that the widgets are laid out, you have a Row
with a TextFormField
and a StyledButton
, which contains a Row
. Also note the TextFormField
is wrapped in an Expanded
widget, which forces the TextFormField
to fill any extra space in the row. To better understand why this is required, see 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.
- Edit the body of
HomePage
to add the following two lines at the end of theListView
'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 update this code shortly to make it functional.
App preview
When a user clicks SEND , it triggers the following code snippet. 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 in the guestbook
collection.
Note that FirebaseAuth.instance.currentUser.uid
is a reference to the autogenerated unique ID that Authentication gives for all logged-in users.
- In the
lib/app_state.dart
file, add theaddMessageToGuestBook
method. You connect this capability with the user interface in the next step.
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.
}
Connect UI and 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 Firestore. Now all you need to do is connect the two.
- In the
lib/home_page.dart
file, make the following change to theHomePage
widget:
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.
],
),
);
}
}
You replaced the two lines that you added at the start of this step with the full implementation. You again use Consumer<ApplicationState>
to make the app state available to the part of the tree that you render. This lets you react to someone who enters a message in the UI and publish it in the database. In the next section, you test whether the added messages are published in the database.
Test sending messages
- If necessary, sign in to the app.
- Enter a message, such as
Hey there!
, and then click SEND .
This action writes the message to your Firestore database. However, you don't see the message in your actual Flutter app because you still need to implement retrieval of the data, which you do in the next step. However, in the Firebase console's Database dashboard , you can see your added message in the guestbook
collection. If you send more messages, you add more documents to your guestbook
collection. For example, see the following code snippet:
7. Read messages
It's lovely that guests can write messages to the database, but they can't see them in the app yet. Time to fix that!
Synchronize messages
To display messages, you need to add listeners that trigger when data changes and then create a UI element that shows new messages. You add code to the app state that listens for newly added messages from the app.
- Create a new file
guest_book_message.dart
, add the following class to expose a structured view of the data that you store in Firestore.
lib/guest_book_message.dart
class GuestBookMessage {
GuestBookMessage({required this.name, required this.message});
final String name;
final String message;
}
- In the
lib/app_state.dart
file, add the following imports:
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
- In section of
ApplicationState
where you define state and getters, add the following lines:
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.
- In the initialization section of
ApplicationState
, add the following lines to subscribe to a query over the document collection when a user logs in and unsubscribe when they log out:
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();
});
}
This section is important because it's 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 that you can unsubscribe from it later. There's a lot going on here, so you should explore it in a debugger to inspect what happens to get a clearer mental model. For more information, see Get realtime updates with Firestore .
- In the
lib/guest_book.dart
file, add the following import:
import 'guest_book_message.dart';
- In the
GuestBook
widget, add a list of messages as part of the configuration to connect this changing state to the user interface:
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();
}
- In
_GuestBookState
, modify thebuild
method as follows to expose this 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.
);
}
}
You wrap the previous content of the build()
method with a Column
widget and then you add a collection for at the tail of the Column
's children to generate a new Paragraph
for each message in the list of messages.
- Update the body of
HomePage
to correctly constructGuestBook
with the newmessages
parameter:
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
),
],
],
),
),
Test message synchronization
Firestore automatically and instantly synchronizes data with clients subscribed to the database.
Test message synchronization:
- In the app, find the messages that you created earlier in the database.
- Write new messages. They appear instantly.
- Open your workspace in multiple windows or tabs. The messages sync in real time across the windows and tabs.
- Optional: In the Firebase console's Database menu, manually delete, modify, or add new messages. All changes appear in the UI.
Congratulations! You read Firestore documents in your app!
App preview
8. Set up basic security rules
You initially set up Firestore to use test mode, which means that your database is open for reads and writes. However, you should only use test mode during early stages of development. As a best practice, you should set up security rules for your database as you develop your app. Security is integral to your app's structure and behavior.
Firebase Security Rules let you control access to documents and collections in your database. The flexible rules syntax lets you create rules that match anything from all writes to the entire database to operations on a specific document.
Set up basic security rules:
- In the Firebase console's Develop menu, click Database > Rules . You should see the following default security rules and a warning about the rules being public:
- 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.
}
}
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:
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, only signed-in users can read messages in the guest book, but only a message's author can edit a message.
- Add data validation to ensure that all 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 only allows people to chat when they're interested in the event. Also, the only way that you know whether someone's coming is when they say so in the chat.
In this step, you get organized and let people know how many people are coming. You add a couple of capabilities to the app state. The first is the ability for a logged-in user to nominate whether they're attending. The second is a counter of how many people are attending.
- In the
lib/app_state.dart
file, add the following lines to the accessors section of theApplicationState
so that the UI code can interact with this state:
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});
}
}
- Update the
ApplicationState
'sinit()
method as follows:
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) {
_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();
});
}
This code adds an always-subscribed query to determine the number of attendees and a second query that's only active while a user is logged in to determine whether the user is attending.
- Add the following enumeration at the top of the
lib/app_state.dart
file.
lib/app_state.dart
enum Attending { yes, no, unknown }
- Create a new file
yes_no_selection.dart
, define a new widget that acts like radio buttons:
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: [
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'),
),
],
),
);
}
}
}
It starts in an indeterminate state with neither Yes nor No selected. Once the user selects whether they're attending, you show that option highlighted with a filled button and the other option recedes with a flat rendering.
- Update
HomePage
'sbuild()
method to take advantage ofYesNoSelection
, enable a logged-in user to nominate whether they're attending, and display the number of attendees for the event:
lib/home_page.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.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
You already set up some rules, so the data that you add with the buttons will be rejected. You need to update the rules to allow additions to the attendees
collection.
- In the
attendees
collection, grab the Authentication UID that you used as the document name and verify that the submitter'suid
is the same as the document they're writing:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// ... //
match /attendees/{userId} {
allow read: if true;
allow write: if request.auth.uid == userId;
}
}
}
This lets everyone read the attendees list because there's no private data there, but only the creator can update it.
- Add data validation to ensure that all 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: In the app, click buttons to see the results in the Firestore dashboard in the Firebase console.
App preview
10. Congratulations!
You used Firebase to build an interactive, real-time web app!