1. Prima di iniziare
In questo codelab imparerai alcune nozioni di base di Firebase per creare app mobile Flutter per Android e iOS.
Prerequisiti
- Familiarità con Flutter
- L'SDK Flutter
- Un editor di testo a tua scelta
Obiettivi didattici
- Come creare un'app di chat per la risposta all'invito a un evento e il libro degli ospiti su Android, iOS, web e macOS con Flutter.
- Come autenticare gli utenti con Firebase Authentication e sincronizzare i dati con Firestore.
Che cosa ti serve
Uno dei seguenti dispositivi:
- Un dispositivo Android o iOS fisico connesso al computer e impostato sulla modalità sviluppatore.
- Il simulatore iOS (richiede gli strumenti Xcode).
- L'emulatore Android (richiede la configurazione in Android Studio).
Inoltre, devi disporre di quanto segue:
- Un browser a tua scelta, ad esempio Google Chrome.
- Un IDE o un editor di testo a tua scelta configurato con i plug-in Dart e Flutter, ad esempio Android Studio o Visual Studio Code.
- L'ultima versione
stable
di Flutter obeta
se ti piace vivere al limite. - Un Account Google per la creazione e la gestione del tuo progetto Firebase.
- La CLI
Firebase
ha eseguito l'accesso al tuo Account Google.
2. recupera il codice campione
Scarica la versione iniziale del progetto da GitHub:
- Dalla riga di comando, clona il repository GitHub nella directory
flutter-codelabs
:
git clone https://github.com/flutter/codelabs.git flutter-codelabs
La directory flutter-codelabs
contiene il codice per una raccolta di codelab. Il codice per questo codelab si trova nella directory flutter-codelabs/firebase-get-to-know-flutter
. La directory contiene una serie di snapshot che mostrano l'aspetto del progetto al termine di ogni passaggio. Ad esempio, ti trovi al secondo passaggio.
- Trova i file corrispondenti per il secondo passaggio:
cd flutter-codelabs/firebase-get-to-know-flutter/step_02
Se vuoi andare avanti o vedere come dovrebbe apparire qualcosa dopo un passaggio, cerca nella directory denominata in base al passaggio che ti interessa.
Importa l'app iniziale
- Apri o importa la directory
flutter-codelabs/firebase-get-to-know-flutter/step_02
nell'IDE che preferisci. Questa directory contiene il codice iniziale del codelab, che consiste in un'app per meetup Flutter non ancora funzionante.
Individuare i file da modificare
Il codice di questa app è distribuito su più directory. Questa suddivisione delle funzionalità semplifica il lavoro perché raggruppa il codice in base alla funzionalità.
- Individua i seguenti file:
lib/main.dart
: questo file contiene il punto di ingresso principale e il widget dell'app.lib/home_page.dart
: questo file contiene il widget della home page.lib/src/widgets.dart
: questo file contiene una serie di widget per standardizzare lo stile dell'app. Compongono la schermata dell'app iniziale.lib/src/authentication.dart
: questo file contiene un'implementazione parziale dell'autenticazione con un insieme di widget per creare un'esperienza utente di accesso per l'autenticazione basata su email di Firebase. Questi widget per il flusso di autenticazione non vengono ancora utilizzati nell'app iniziale, ma li aggiungerai a breve.
Aggiungi altri file in base alle esigenze per creare il resto dell'app.
Esamina il file lib/main.dart
Questa app sfrutta il pacchetto google_fonts
per impostare Roboto come carattere predefinito in tutta l'app. Puoi esplorare fonts.google.com e utilizzare i caratteri che trovi in diverse parti dell'app.
Utilizzi i widget helper del file lib/src/widgets.dart
sotto forma di Header
, Paragraph
e IconAndDetail
. Questi widget eliminano il codice duplicato per ridurre il disordine nel layout di pagina descritto in HomePage
. In questo modo, si ottiene anche un aspetto coerente.
Ecco l'aspetto della tua app su Android, iOS, web e macOS:
3. Crea e configura un progetto Firebase
La visualizzazione delle informazioni sugli eventi è ottima per i tuoi ospiti, ma non è molto utile da sola. Devi aggiungere alcune funzionalità dinamiche all'app. Per farlo, devi connettere Firebase alla tua app. Per iniziare a utilizzare Firebase, devi creare e configurare un progetto Firebase.
Crea un progetto Firebase
- Accedi alla console Firebase utilizzando il tuo Account Google.
- Fai clic sul pulsante per creare un nuovo progetto, quindi inserisci un nome per il progetto (ad esempio
Firebase-Flutter-Codelab
).
- Fai clic su Continua.
- Se richiesto, leggi e accetta i termini di Firebase, quindi fai clic su Continua.
- (Facoltativo) Attiva l'assistenza AI nella console Firebase (denominata "Gemini in Firebase").
- Per questo codelab non hai bisogno di Google Analytics, quindi disattiva l'opzione Google Analytics.
- Fai clic su Crea progetto, attendi il provisioning del progetto, poi fai clic su Continua.
Per saperne di più sui progetti Firebase, consulta Informazioni sui progetti Firebase.
Configura i prodotti Firebase
L'app utilizza i seguenti prodotti Firebase, disponibili per le app web:
- Autenticazione:consente agli utenti di accedere alla tua app.
- Firestore: salva i dati strutturati sul cloud e riceve notifiche istantanee quando i dati cambiano.
- Regole di sicurezza Firebase:proteggono il tuo database.
Alcuni di questi prodotti richiedono una configurazione speciale o devono essere abilitati nella console Firebase.
Attivare l'autenticazione per l'accesso con email
- Nel riquadro Panoramica del progetto della console Firebase, espandi il menu Build.
- Fai clic su Autenticazione > Inizia > Metodo di accesso > Email/Password > Attiva > Salva.
Configura Firestore
L'app web utilizza Firestore per salvare i messaggi di chat e riceverne di nuovi.
Ecco come configurare Firestore nel tuo progetto Firebase:
- Nel riquadro a sinistra della console Firebase, espandi Build e seleziona Database Firestore.
- Fai clic su Crea database.
- Lascia l'ID database impostato su
(default)
. - Seleziona una posizione per il database, poi fai clic su Avanti.
Per un'app reale, devi scegliere una posizione vicina ai tuoi utenti. - Fai clic su Avvia in modalità di test. Leggi l'esclusione di responsabilità relativa alle regole di sicurezza.
Più avanti in questo codelab, aggiungerai regole di sicurezza per proteggere i tuoi dati. Non distribuire o esporre pubblicamente un'app senza aggiungere regole di sicurezza per il tuo database. - Fai clic su Crea.
4. Configura Firebase
Per utilizzare Firebase con Flutter, devi completare le seguenti attività per configurare il progetto Flutter in modo da utilizzare correttamente le librerie FlutterFire
:
- Aggiungi le dipendenze
FlutterFire
al tuo progetto. - Registra la piattaforma che preferisci nel progetto Firebase.
- Scarica il file di configurazione specifico della piattaforma e poi aggiungilo al codice.
Nella directory di primo livello dell'app Flutter, sono presenti le sottodirectory android
, ios
, macos
e web
, che contengono i file di configurazione specifici della piattaforma per iOS e Android, rispettivamente.
Configura le dipendenze
Devi aggiungere le librerie FlutterFire
per i due prodotti Firebase che utilizzi in questa app: Authentication e Firestore.
- Dalla riga di comando, aggiungi le seguenti dipendenze:
$ flutter pub add firebase_core
Il firebase_core
package è il codice comune richiesto per tutti i plug-in Firebase Flutter.
$ flutter pub add firebase_auth
Il pacchetto firebase_auth
consente l'integrazione con l'autenticazione.
$ flutter pub add cloud_firestore
Il pacchetto cloud_firestore
consente l'accesso all'archiviazione dei dati di Firestore.
$ flutter pub add provider
Il pacchetto firebase_ui_auth
fornisce un insieme di widget e utilità per aumentare la velocità degli sviluppatori con i flussi di autenticazione.
$ flutter pub add firebase_ui_auth
Hai aggiunto i pacchetti richiesti, ma devi anche configurare i progetti runner iOS, Android, macOS e web per utilizzare Firebase in modo appropriato. Utilizzi anche il pacchetto provider
che consente di separare la logica di business dalla logica di visualizzazione.
Installa l'interfaccia a riga di comando FlutterFire
L'interfaccia a riga di comando FlutterFire dipende dall'interfaccia a riga di comando di Firebase sottostante.
- Se non l'hai ancora fatto, installa la CLI di Firebase sulla tua macchina.
- Installa l'interfaccia a riga di comando FlutterFire:
$ dart pub global activate flutterfire_cli
Una volta installato, il comando flutterfire
è disponibile a livello globale.
Configurare le app
L'interfaccia a riga di comando estrae le informazioni dal progetto Firebase e dalle app del progetto selezionate per generare tutta la configurazione per una piattaforma specifica.
Nella directory principale dell'app, esegui il comando configure
:
$ flutterfire configure
Il comando di configurazione ti guida attraverso le seguenti procedure:
- Seleziona un progetto Firebase in base al file
.firebaserc
o dalla console Firebase. - Determina le piattaforme per la configurazione, ad esempio Android, iOS, macOS e web.
- Identifica le app Firebase da cui estrarre la configurazione. Per impostazione predefinita, la CLI tenta di abbinare automaticamente le app Firebase in base alla configurazione attuale del progetto.
- Genera un file
firebase_options.dart
nel tuo progetto.
Configura macOS
Flutter su macOS crea app completamente in sandbox. Poiché questa app si integra con la rete per comunicare con i server Firebase, devi configurarla con i privilegi del client di rete.
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>
Per ulteriori informazioni, vedi Supporto del desktop per Flutter.
5. Aggiungere la funzionalità di risposta
Ora che hai aggiunto Firebase all'app, puoi creare un pulsante RSVP che registra le persone con Authentication. Per Android nativo, iOS nativo e web, sono disponibili pacchetti FirebaseUI Auth
predefiniti, ma devi creare questa funzionalità per Flutter.
Il progetto che hai recuperato in precedenza includeva un insieme di widget che implementano l'interfaccia utente per la maggior parte del flusso di autenticazione. Implementi la logica di business per integrare l'autenticazione con l'app.
Aggiungere la logica di business con il pacchetto Provider
Utilizza il pacchetto provider
per rendere disponibile un oggetto di stato dell'app centralizzato in tutto l'albero dei widget Flutter dell'app:
- Crea un nuovo file denominato
app_state.dart
con i seguenti contenuti:
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();
});
}
}
Le istruzioni import
introducono Firebase Core e Auth, recuperano il pacchetto provider
che rende disponibile l'oggetto stato dell'app in tutto l'albero dei widget e includono i widget di autenticazione dal pacchetto firebase_ui_auth
.
Questo oggetto di stato dell'applicazione ApplicationState
ha una responsabilità principale per questo passaggio, ovvero avvisare l'albero dei widget che è stato aggiornato uno stato autenticato.
Utilizzi un provider solo per comunicare lo stato di accesso di un utente all'app. Per consentire a un utente di accedere, utilizzi le UI fornite dal pacchetto firebase_ui_auth
, che è un ottimo modo per avviare rapidamente le schermate di accesso nelle tue app.
Integrare il flusso di autenticazione
- Modifica le importazioni nella parte superiore del file
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';
- Collega lo stato dell'app all'inizializzazione dell'app e poi aggiungi il flusso di autenticazione a
HomePage
:
lib/main.dart
void main() {
// Modify from here...
WidgetsFlutterBinding.ensureInitialized();
runApp(ChangeNotifierProvider(
create: (context) => ApplicationState(),
builder: ((context, child) => const App()),
));
// ...to here.
}
La modifica alla funzione main()
rende il pacchetto del provider responsabile dell'istanza dell'oggetto stato dell'app con il widget ChangeNotifierProvider
. Utilizzi questa classe provider
specifica perché l'oggetto stato dell'app estende la classe ChangeNotifier
, che consente al pacchetto provider
di sapere quando visualizzare nuovamente i widget dipendenti.
- Aggiorna la tua app per gestire la navigazione verso le diverse schermate fornite da FirebaseUI creando una configurazione
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
);
}
}
A ogni schermata è associato un tipo di azione diverso in base al nuovo stato del flusso di autenticazione. Dopo la maggior parte dei cambi di stato nell'autenticazione, puoi reindirizzare a una schermata preferita, che si tratti della schermata Home o di un'altra schermata, ad esempio il profilo.
- Nel metodo build della classe
HomePage
, integra lo stato dell'app con il 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!',
),
],
),
);
}
}
Istanzia il widget AuthFunc
e lo racchiudi in un widget Consumer
. Il widget Consumer è il modo consueto in cui il pacchetto provider
può essere utilizzato per ricostruire parte dell'albero quando lo stato dell'app cambia. Il widget AuthFunc
è il widget supplementare che testi.
Testare il flusso di autenticazione
- Nell'app, tocca il pulsante Rispondi per avviare la
SignInScreen
.
- Inserisci un indirizzo email. Se hai già effettuato la registrazione, il sistema ti chiede di inserire una password. In caso contrario, il sistema ti chiede di compilare il modulo di registrazione.
- Inserisci una password composta da meno di sei caratteri per verificare il flusso di gestione degli errori. Se hai eseguito la registrazione, visualizzerai la password.
- Inserisci password errate per controllare il flusso di gestione degli errori.
- Inserisci la password corretta. Visualizzi l'esperienza di accesso, che offre all'utente la possibilità di uscire.
6. Scrivere messaggi in Firestore
È fantastico sapere che gli utenti stanno arrivando, ma devi dare agli ospiti qualcosa da fare nell'app. Cosa succederebbe se potessero lasciare messaggi in un guestbook? Possono condividere il motivo per cui sono entusiasti di partecipare o chi sperano di incontrare.
Per archiviare i messaggi di chat che gli utenti scrivono nell'app, utilizzi Firestore.
Modello dei dati
Firestore è un database NoSQL e i dati archiviati nel database sono suddivisi in raccolte, documenti, campi e raccolte secondarie. Ogni messaggio della chat viene archiviato come documento in una raccolta guestbook
, che è una raccolta di primo livello.
Aggiungere messaggi a Firestore
In questa sezione, aggiungi la funzionalità che consente agli utenti di scrivere messaggi nel database. Innanzitutto, aggiungi un campo del modulo e un pulsante di invio, quindi aggiungi il codice che collega questi elementi al database.
- Crea un nuovo file denominato
guest_book.dart
, aggiungi un widget statefulGuestBook
per creare gli elementi dell'interfaccia utente di un campo del messaggio e un pulsante di invio:
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'),
],
),
),
],
),
),
);
}
}
Ci sono un paio di punti di interesse qui. Innanzitutto, crei un'istanza di un modulo per poter verificare che il messaggio contenga effettivamente contenuti e mostrare all'utente un messaggio di errore in caso contrario. Per convalidare un modulo, accedi allo stato del modulo dietro il modulo con un GlobalKey
. Per saperne di più sulle chiavi e su come utilizzarle, consulta Quando utilizzare le chiavi.
Nota anche la disposizione dei widget: hai un Row
con un TextFormField
e un StyledButton
, che contiene un Row
. Tieni presente anche che TextFormField
è racchiuso in un widget Expanded
, che forza TextFormField
a riempire lo spazio aggiuntivo nella riga. Per capire meglio perché è necessario, vedi Informazioni sui vincoli.
Ora che hai un widget che consente all'utente di inserire del testo da aggiungere al Guest Book, devi visualizzarlo sullo schermo.
- Modifica il corpo di
HomePage
per aggiungere le seguenti due righe alla fine dei figli diListView
:
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)),
Sebbene sia sufficiente per visualizzare il widget, non è sufficiente per fare qualcosa di utile. Aggiornerai questo codice a breve per renderlo funzionale.
Anteprima dell'app
Quando un utente fa clic su INVIA, viene attivato lo snippet di codice riportato di seguito. Aggiunge i contenuti del campo di input del messaggio alla raccolta guestbook
del database. Nello specifico, il metodo addMessageToGuestBook
aggiunge i contenuti del messaggio a un nuovo documento con un ID generato automaticamente nella raccolta guestbook
.
Tieni presente che FirebaseAuth.instance.currentUser.uid
fa riferimento all'ID univoco generato automaticamente che Authentication fornisce per tutti gli utenti che hanno eseguito l'accesso.
- Nel file
lib/app_state.dart
, aggiungi il metodoaddMessageToGuestBook
. Nel passaggio successivo, collegherai questa funzionalità all'interfaccia utente.
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.
}
Connettere l'interfaccia utente e il database
Hai un'interfaccia utente in cui l'utente può inserire il testo che vuole aggiungere al Guest Book e il codice per aggiungere la voce a Firestore. Ora non devi fare altro che collegarli.
- Nel file
lib/home_page.dart
, apporta la seguente modifica al 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.
],
),
);
}
}
Hai sostituito le due righe che hai aggiunto all'inizio di questo passaggio con l'implementazione completa. Utilizzi di nuovo Consumer<ApplicationState>
per rendere disponibile lo stato dell'app alla parte dell'albero che esegui il rendering. In questo modo, puoi reagire a un utente che inserisce un messaggio nell'UI e pubblicarlo nel database. Nella sezione successiva, verificherai se i messaggi aggiunti vengono pubblicati nel database.
Testare l'invio di messaggi
- Se necessario, accedi all'app.
- Inserisci un messaggio, ad esempio
Hey there!
, e poi fai clic su INVIA.
Questa azione scrive il messaggio nel database Firestore. Tuttavia, non vedi il messaggio nella tua app Flutter perché devi ancora implementare il recupero dei dati, cosa che farai nel passaggio successivo. Tuttavia, nella dashboardDatabase della console Firebase, puoi visualizzare il messaggio aggiunto nella raccolta guestbook
. Se invii altri messaggi, aggiungi altri documenti alla tua raccolta guestbook
. Ad esempio, vedi il seguente snippet di codice:
7. Leggere i messaggi
È bello che gli ospiti possano scrivere messaggi nel database, ma non possono ancora vederli nell'app. È ora di risolvere il problema.
Sincronizzare i messaggi
Per visualizzare i messaggi, devi aggiungere listener che si attivano quando i dati cambiano e poi creare un elemento UI che mostri i nuovi messaggi. Aggiungi codice allo stato dell'app che ascolta i messaggi appena aggiunti dall'app.
- Crea un nuovo file
guest_book_message.dart
, aggiungi la seguente classe per esporre una visualizzazione strutturata dei dati archiviati in Firestore.
lib/guest_book_message.dart
class GuestBookMessage {
GuestBookMessage({required this.name, required this.message});
final String name;
final String message;
}
- Nel file
lib/app_state.dart
, aggiungi le seguenti importazioni:
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
- Nella sezione di
ApplicationState
in cui definisci lo stato e i getter, aggiungi le seguenti righe:
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.
- Nella sezione di inizializzazione di
ApplicationState
, aggiungi le seguenti righe per abbonarti a una query sulla raccolta di documenti quando un utente esegue l'accesso e annullare l'abbonamento quando esce:
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();
});
}
Questa sezione è importante perché è qui che costruisci una query sulla raccolta guestbook
e gestisci l'iscrizione e l'annullamento dell'iscrizione a questa raccolta. Ascolti lo stream, dove ricostruisci una cache locale dei messaggi nella raccolta guestbook
e memorizzi anche un riferimento a questo abbonamento in modo da poterti annullare l'iscrizione in un secondo momento. Qui succedono molte cose, quindi dovresti esplorare il codice in un debugger per capire meglio cosa succede. Per ulteriori informazioni, consulta Ricevere aggiornamenti in tempo reale con Firestore.
- Nel file
lib/guest_book.dart
, aggiungi la seguente importazione:
import 'guest_book_message.dart';
- Nel widget
GuestBook
, aggiungi un elenco di messaggi nell'ambito della configurazione per collegare questo stato variabile all'interfaccia utente:
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
, modifica il metodobuild
come segue per esporre questa configurazione:
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.
);
}
}
Racchiudi i contenuti precedenti del metodo build()
con un widget Column
e poi aggiungi una raccolta per alla fine dei figli di Column
per generare un nuovo Paragraph
per ogni messaggio nell'elenco dei messaggi.
- Aggiorna il corpo di
HomePage
per creare correttamenteGuestBook
con il nuovo parametromessages
:
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
),
],
],
),
),
Testare la sincronizzazione dei messaggi
Firestore sincronizza automaticamente e istantaneamente i dati con i client iscritti al database.
Testare la sincronizzazione dei messaggi:
- Nell'app, trova i messaggi che hai creato in precedenza nel database.
- Scrivere nuovi messaggi. Vengono visualizzati immediatamente.
- Apri lo spazio di lavoro in più finestre o schede. I messaggi vengono sincronizzati in tempo reale nelle finestre e nelle schede.
- (Facoltativo) Nel menu Database della console Firebase, elimina, modifica o aggiungi manualmente nuovi messaggi. Tutte le modifiche vengono visualizzate nella UI.
Complimenti! Hai letto i documenti Firestore nella tua app.
Anteprima dell'app
8. Configurare regole di sicurezza di base
Inizialmente hai configurato Firestore per utilizzare la modalità di test, il che significa che il tuo database è aperto per le operazioni di lettura e scrittura. Tuttavia, devi utilizzare la modalità di test solo nelle prime fasi dello sviluppo. Come best practice, devi configurare le regole di sicurezza per il database durante lo sviluppo dell'app. La sicurezza è parte integrante della struttura e del comportamento dell'app.
Le regole di sicurezza di Firebase ti consentono di controllare l'accesso a documenti e raccolte nel tuo database. La sintassi flessibile delle regole ti consente di creare regole che corrispondono a qualsiasi cosa, da tutte le scritture all'intero database alle operazioni su un documento specifico.
Configura regole di sicurezza di base:
- Nel menu Sviluppa della console Firebase, fai clic su Database > Regole. Dovresti visualizzare le seguenti regole di sicurezza predefinite e un avviso che indica che le regole sono pubbliche:
- Identifica le raccolte in cui l'app scrive i dati:
In match /databases/{database}/documents
, identifica la raccolta che vuoi proteggere:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /guestbook/{entry} {
// You'll add rules here in the next step.
}
}
Poiché hai utilizzato l'UID di autenticazione come campo in ogni documento del guestbook, puoi ottenere l'UID di autenticazione e verificare che chiunque tenti di scrivere nel documento abbia un UID di autenticazione corrispondente.
- Aggiungi le regole di lettura e scrittura al set di regole:
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;
}
}
}
Ora, solo gli utenti che hanno eseguito l'accesso possono leggere i messaggi nel guestbook, ma solo l'autore di un messaggio può modificarlo.
- Aggiungi la convalida dei dati per assicurarti che tutti i campi previsti siano presenti nel documento:
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. Passaggio bonus: metti in pratica ciò che hai imparato
Registrare lo stato di partecipazione di un partecipante
Al momento, la tua app consente alle persone di chattare solo quando sono interessate all'evento. Inoltre, l'unico modo per sapere se qualcuno sta arrivando è quando lo dice nella chat.
In questo passaggio, ti organizzi e comunichi alle persone quante persone parteciperanno. Aggiungi un paio di funzionalità allo stato dell'app. La prima è la possibilità per un utente che ha eseguito l'accesso di indicare se parteciperà. Il secondo è un contatore del numero di partecipanti.
- Nel file
lib/app_state.dart
, aggiungi le seguenti righe alla sezione degli accessor diApplicationState
in modo che il codice della UI possa interagire con questo stato:
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});
}
}
- Aggiorna il metodo
init()
diApplicationState
come segue:
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();
});
}
Questo codice aggiunge una query sempre attiva per determinare il numero di partecipanti e una seconda query attiva solo quando un utente ha eseguito l'accesso per determinare se l'utente partecipa.
- Aggiungi la seguente enumerazione all'inizio del file
lib/app_state.dart
.
lib/app_state.dart
enum Attending { yes, no, unknown }
- Crea un nuovo file
yes_no_selection.dart
, definisci un nuovo widget che funge da pulsanti di opzione:
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'),
),
],
),
);
}
}
}
Inizialmente, lo stato è indeterminato e non è selezionato né Sì né No. Una volta che l'utente ha selezionato se parteciperà, l'opzione viene evidenziata con un pulsante pieno, mentre l'altra opzione viene visualizzata con un rendering piatto.
- Aggiorna il metodo
build()
diHomePage
per sfruttareYesNoSelection
, consentire a un utente che ha eseguito l'accesso di indicare se parteciperà e visualizzare il numero di partecipanti all'evento:
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,
),
],
],
),
),
Aggiungi regole
Hai già configurato alcune regole, quindi i dati che aggiungi con i pulsanti verranno rifiutati. Devi aggiornare le regole per consentire le aggiunte alla raccolta attendees
.
- Nella raccolta
attendees
, prendi l'UID di autenticazione che hai utilizzato come nome del documento e verifica che l'uid
dell'autore dell'invio sia lo stesso del documento che sta scrivendo:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// ... //
match /attendees/{userId} {
allow read: if true;
allow write: if request.auth.uid == userId;
}
}
}
In questo modo, tutti possono leggere l'elenco dei partecipanti perché non contiene dati privati, ma solo il creator può aggiornarlo.
- Aggiungi la convalida dei dati per assicurarti che tutti i campi previsti siano presenti nel documento:
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;
}
}
}
- (Facoltativo) Nell'app, fai clic sui pulsanti per visualizzare i risultati nella dashboard Firestore della console Firebase.
Anteprima dell'app
10. Complimenti!
Hai utilizzato Firebase per creare un'app web interattiva in tempo reale.