Informazioni su Firebase per Flutter

1. Prima di iniziare

In questo codelab apprenderai alcune nozioni di base di Firebase per creare app mobile Flutter per Android e iOS.

Prerequisiti

Obiettivi didattici

  • Come creare un'app di chat per la risposta agli inviti e il libro degli ospiti di un evento su Android, iOS, il web e macOS con Flutter.
  • Come autenticare gli utenti con Firebase Authentication e sincronizzare i dati con Firestore.

La schermata Home dell'app su Android

La schermata Home dell'app su iOS

Che cosa ti serve

Uno dei seguenti dispositivi:

  • Un dispositivo fisico Android o iOS collegato al computer e impostato sulla modalità sviluppatore.
  • Il simulatore iOS (sono necessari gli strumenti Xcode).
  • L'emulatore Android (è necessaria la configurazione in Android Studio).

Sono inoltre necessari:

  • 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 o beta, se ti piace vivere in un ambito perimetrale.
  • Un Account Google per la creazione e la gestione di un progetto Firebase.
  • La Firebase CLI ha eseguito l'accesso al tuo Account Google.

2. recupera il codice campione

Scarica la versione iniziale del tuo progetto da GitHub:

  1. 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 di una raccolta di codelab. Il codice di questo codelab si trova nella directory flutter-codelabs/firebase-get-to-know-flutter. La directory contiene una serie di istantanee che mostrano l'aspetto del progetto al termine di ogni passaggio. Ad esempio, sei al secondo passaggio.

  1. 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 di avvio

  • Apri o importa la directory flutter-codelabs/firebase-get-to-know-flutter/step_02 nell'IDE che preferisci. Questa directory contiene il codice di avvio per il codelab, che consiste in un'app di meetup Flutter non ancora funzionale.

Individuare i file che richiedono modifiche

Il codice in questa app è distribuito su più directory. Questa suddivisione delle funzionalità semplifica il lavoro perché raggruppa il codice per 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 alcuni widget che contribuiscono a standardizzare lo stile dell'app. Questi widget compongono la schermata dell'app iniziale.
    • lib/src/authentication.dart: questo file contiene un'implementazione parziale di Autenticazione con una serie di widget per creare un'esperienza utente di accesso per l'autenticazione basata su email Firebase. Questi widget per il flusso di autenticazione non sono ancora utilizzati nell'app iniziale, ma li aggiungerai a breve.

Se necessario, aggiungi altri file per creare il resto dell'app.

Esamina il file lib/main.dart

Questa app si avvale del pacchetto google_fonts per rendere Roboto il carattere predefinito dell'app. Puoi esplorare il sito fonts.google.com e utilizzare i caratteri che trovi al suo interno in diverse parti dell'app.

Utilizzi i widget di supporto del file lib/src/widgets.dart nel formato Header, Paragraph e IconAndDetail. Questi widget eliminano il codice duplicato per ridurre il disordine nel layout di pagina descritto in HomePage. Ciò garantisce anche un aspetto e un design coerenti.

Ecco l'aspetto della tua app su Android, iOS, Web e macOS:

La schermata Home dell'app su Android

La schermata Home dell'app su iOS

La schermata Home dell'app sul web

La schermata Home dell'app su macOS

3. Crea e configura un progetto Firebase

La visualizzazione delle informazioni sull'evento è ottima per i tuoi invitati, ma da sola non è molto utile per nessuno. Devi aggiungere alcune funzionalità dinamiche all'app. Per farlo, devi collegare Firebase alla tua app. Per iniziare a utilizzare Firebase, devi creare e configurare un progetto Firebase.

Crea un progetto Firebase

  1. Accedi a Firebase.
  2. Nella console, fai clic su Aggiungi progetto o Crea un progetto.
  3. Nel campo Nome progetto, inserisci Firebase-Flutter-Codelab e fai clic su Continua.

4395e4e67c08043a.png

  1. Fai clic sulle opzioni di creazione del progetto. Se richiesto, accetta i termini di Firebase, ma salta la configurazione di Google Analytics perché non lo utilizzerai per questa app.

b7138cde5f2c7b61.png

Per saperne di più sui progetti Firebase, consulta Informazioni sui progetti 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 immediate quando i dati cambiano.
  • Regole di sicurezza Firebase: proteggono il database.

Alcuni di questi prodotti richiedono una configurazione speciale o devi abilitarli nella console Firebase.

Attiva l'autenticazione dell'accesso alle email

  1. Nel riquadro Panoramica del progetto della console Firebase, espandi il menu Crea.
  2. Fai clic su Autenticazione > Inizia > Metodo di accesso > Email/password > Attiva > Salva.

58e3e3e23c2f16a4.png

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:

  1. Nel riquadro a sinistra della console Firebase, espandi Build e seleziona Database Firestore.
  2. Fai clic su Crea database.
  3. Lascia l'ID database impostato su (default).
  4. Seleziona una posizione per il database, poi fai clic su Avanti.
    Per un'app reale, scegli una posizione vicina ai tuoi utenti.
  5. Fai clic su Avvia in modalità di test. Leggi il disclaimer relativo alle regole di sicurezza.
    Più avanti in questo codelab aggiungerai le regole di sicurezza per proteggere i tuoi dati. Non distribuire o esporre pubblicamente un'app senza aggiungere regole di sicurezza per il tuo database.
  6. 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:

  1. Aggiungi le dipendenze FlutterFire al progetto.
  2. Registra la piattaforma che ti interessa nel progetto Firebase.
  3. Scarica il file di configurazione specifico della piattaforma e 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 rispettivamente per iOS e Android.

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 pacchetto firebase_core è il codice comune richiesto per tutti i plug-in Firebase Flutter.

$ flutter pub add firebase_auth

Il pacchetto firebase_auth abilita l'integrazione con Authentication.

$ flutter pub add cloud_firestore

Il pacchetto cloud_firestore consente di accedere allo spazio di archiviazione dei dati 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 iOS, Android, macOS e Web Runner per utilizzare Firebase in modo appropriato. Puoi anche utilizzare 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.

  1. Se non l'hai ancora fatto, installa l'interfaccia a riga di comando di Firebase sulla tua macchina.
  2. Installa l'interfaccia a riga di comando FlutterFire:
$ dart pub global activate flutterfire_cli

Una volta installato, il comando flutterfire è disponibile a livello globale.

Configura le tue app

L'interfaccia a riga di comando estrae le informazioni dal progetto Firebase e dalle app di progetto selezionate per generare tutta la configurazione per una piattaforma specifica.

Nella directory radice dell'app, esegui il comando configure:

$ flutterfire configure

Il comando di configurazione ti guida attraverso le seguenti procedure:

  1. Seleziona un progetto Firebase in base al file .firebaserc o dalla console Firebase.
  2. Determina le piattaforme di configurazione, ad esempio Android, iOS, macOS e web.
  3. Identifica le app Firebase da cui estrarre la configurazione. Per impostazione predefinita, l'interfaccia a riga di comando tenta di abbinare automaticamente le app Firebase in base alla configurazione attuale del progetto.
  4. Genera un file firebase_options.dart nel 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 configurare l'app 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, consulta Supporto desktop per Flutter.

5. Aggiungere la funzionalità di risposta

Ora che hai aggiunto Firebase all'app, puoi creare un pulsante Rispondi che registri le persone con Authentication. Per gli annunci nativi Android, nativi iOS e web, sono disponibili pacchetti FirebaseUI Auth predefiniti, ma è necessario sviluppare 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. Devi implementare la logica di business per integrare Authentication con l'app.

Aggiungi la logica di business con il pacchetto Provider

Utilizza il pacchetto provider per rendere disponibile un oggetto dello stato dell'app centralizzato nell'intera struttura ad albero dei widget Flutter dell'app:

  1. Crea un nuovo file denominato app_state.dart con i seguenti contenuti:

lib/app_state.ARROW

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, importano il pacchetto provider che rende disponibile l'oggetto stato dell'app nell'intera struttura ad albero dei widget e includono i widget di autenticazione del pacchetto firebase_ui_auth.

Questo oggetto dello stato dell'applicazione ApplicationState ha un'unica responsabilità principale per questo passaggio, ovvero avvisare la struttura ad albero dei widget che è stato eseguito un aggiornamento a uno stato autenticato.

Devi utilizzare un provider solo per comunicare lo stato dello stato di accesso di un utente all'app. Per consentire a un utente di accedere, utilizza le UI fornite dal pacchetto firebase_ui_auth, che sono un ottimo modo per eseguire rapidamente il bootstrap delle schermate di accesso nelle tue app.

Integrare il flusso di autenticazione

  1. Modifica le importazioni nella parte superiore del file lib/main.dart:

lib/main.ARROW

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

import 'app_state.dart';                                 // new
import 'home_page.dart';
  1. Connetti lo stato dell'app con la relativa inizializzazione, quindi 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'istanziazione dell'oggetto dello 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 mostrare di nuovo i widget dipendenti.

  1. Aggiorna l'app per gestire la navigazione verso le diverse schermate fornite da FirebaseUI creando una configurazione GoRouter:

lib/main.ARROW

// 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 diverso di azione in base al nuovo stato del flusso di autenticazione. Dopo la maggior parte dei cambiamenti di stato nell'autenticazione, puoi tornare a una schermata preferita, che si tratti della schermata Home o di un'altra schermata, come ad esempio il profilo.

  1. Nel metodo di build della classe HomePage, integra lo stato dell'app con il widget AuthFunc:

lib/home_page.ARROW

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!',
          ),
        ],
      ),
    );
  }
}

Crei un'istanza del widget AuthFunc e lo inserisci in un widget Consumer. Il widget consumer è il solito modo con cui il pacchetto provider può essere utilizzato per ricreare parte della struttura ad albero quando lo stato dell'app cambia. Il widget AuthFunc è i widget supplementari che test.

Testare il flusso di autenticazione

cdf2d25e436bd48d.png

  1. Nell'app, tocca il pulsante Rispondi per avviare il SignInScreen.

2a2cd6d69d172369.png

  1. Inserisci un indirizzo email. Se hai già effettuato la registrazione, il sistema ti chiede di inserire una password. In caso contrario, il sistema ti chiederà di compilare il modulo di registrazione.

e5e65065dba36b54.png

  1. Inserisci una password di meno di sei caratteri per controllare il flusso di gestione degli errori. Se hai effettuato la registrazione, viene visualizzata la password.
  2. Inserisci password errate per controllare il flusso di gestione degli errori.
  3. Inserisci la password corretta. Viene visualizzata l'esperienza di accesso, che offre all'utente la possibilità di uscire.

4ed811a25b0cf816.png

6. Scrivi messaggi in Firestore

È fantastico sapere che gli utenti arrivano, ma devi lasciare loro qualcos'altro da fare nell'app. E se potessero lasciare messaggi in un libro degli ospiti? Possono spiegare perché sono entusiasti di venire o chi sperano di incontrare.

Per archiviare i messaggi della chat scritti dagli utenti nell'app, utilizzi Firestore.

Modello dei dati

Firestore è un database NoSQL e i dati archiviati nel database sono suddivisi in raccolte, documenti, campi e sottoraccolte. Ogni messaggio della chat viene archiviato come documento in una raccolta guestbook, ovvero una raccolta di primo livello.

7c20dc8424bb1d84.png

Aggiungi messaggi a Firestore

In questa sezione, aggiungerai la funzionalità per consentire agli utenti di scrivere messaggi nel database. In primo luogo, aggiungi un campo del modulo e un pulsante di invio, quindi aggiungi il codice che collega questi elementi al database.

  1. Crea un nuovo file denominato guest_book.dart, aggiungi un widget stateful GuestBook per creare gli elementi UI di un campo di messaggio e un pulsante di invio:

lib/guest_book.ARROW

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. In primo luogo, crei un'istanza di un modulo in modo da poter verificare che il messaggio contenga effettivamente dei contenuti e, se non ce ne sono, mostrare all'utente un messaggio di errore. 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 inoltre presente che TextFormField è inserito in un widget Expanded, che obbliga TextFormField a riempire lo spazio aggiuntivo nella riga. Per comprendere meglio il motivo per cui è necessario, consulta Informazioni sui vincoli.

Ora che hai un widget che consente all'utente di inserire del testo da aggiungere al Libro degli ospiti, devi visualizzarlo sullo schermo.

  1. Modifica il corpo di HomePage per aggiungere le due righe seguenti alla fine dei figli di ListView:
const Header("What we'll be doing"),
const Paragraph(
  'Join us for a day full of Firebase Workshops and Pizza!',
),
// Add the following two lines.
const Header('Discussion'),
GuestBook(addMessage: (message) => print(message)),

Sebbene sia sufficiente per visualizzare il widget, non è sufficiente per fare qualcosa di utile. A breve aggiornerai questo codice per renderlo funzionale.

Anteprima dell'app

La schermata iniziale dell&#39;app su Android con l&#39;integrazione della chat

La schermata Home dell&#39;app su iOS con integrazione della chat

La schermata Home dell&#39;app sul web con l&#39;integrazione della chat

La schermata Home dell&#39;app su macOS con l&#39;integrazione della chat

Quando un utente fa clic su INVIARE, viene attivato il seguente snippet di codice. Aggiunge i contenuti del campo di immissione del messaggio alla raccolta guestbook del database. In particolare, 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 è un riferimento all'ID univoco generato automaticamente che l'autenticazione fornisce per tutti gli utenti che hanno eseguito l'accesso.

  • Nel file lib/app_state.dart, aggiungi il metodo addMessageToGuestBook. Collega questa funzionalità all'interfaccia utente nel passaggio successivo.

lib/app_state.ARROW

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

Collega l'interfaccia utente e il database

Hai una UI in cui l'utente può inserire il testo che vuole aggiungere al Guest Book e hai il codice per aggiungere la voce a Firestore. Ora non devi fare altro che collegare i due prodotti.

  • Nel file lib/home_page.dart, apporta la seguente modifica al widget HomePage:

lib/home_page.ARROW

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 aggiunte all'inizio di questo passaggio con l'implementazione completa. Utilizza di nuovo Consumer<ApplicationState> per rendere disponibile lo stato dell'app alla parte dell'albero che rendi visibile. In questo modo puoi reagire a un messaggio inserito da un utente nell'interfaccia utente e pubblicarlo nel database. Nella sezione successiva, verificherai se i messaggi aggiunti sono pubblicati nel database.

Testare l'invio di messaggi

  1. Se necessario, accedi all'app.
  2. Inserisci un messaggio, ad esempio Hey there!, e fai clic su INVIA.

Questa azione scrive il messaggio nel database Firestore. Tuttavia, non visualizzi il messaggio nell'app Flutter effettiva perché devi ancora implementare il recupero dei dati, che esegui nel passaggio successivo. Tuttavia, nella dashboard Database della console Firebase, puoi visualizzare il messaggio aggiunto nella raccolta guestbook. Se invii più messaggi, aggiungi altri documenti alla tua raccolta guestbook. Ad esempio, vedi il seguente snippet di codice:

713870af0b3b63c.png

7. Leggere i messaggi

È fantastico che gli ospiti possano scrivere messaggi nel database, ma non possono ancora vederli nell&#39;app. È il momento di risolvere il problema.

Sincronizzare i messaggi

Per visualizzare i messaggi, devi aggiungere ascoltatori che si attivano quando i dati cambiano e poi creare un elemento dell&#39;interfaccia utente che mostri i nuovi messaggi. Aggiungi al codice dello stato dell&#39;app un codice che ascolti i messaggi appena aggiunti dall&#39;app.

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

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

  final String name;
  final String message;
}
  1. Nel file lib/app_state.dart, aggiungi le seguenti importazioni:

lib/app_state.ARROW

import 'dart:async';                                     // new

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

import 'firebase_options.dart';
import 'guest_book_message.dart';                        // new
  1. Nella sezione di ApplicationState in cui definisci lo stato e i getter, aggiungi le seguenti righe:

lib/app_state.ARROW

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

  // Add from here...
  StreamSubscription<QuerySnapshot>? _guestBookSubscription;
  List<GuestBookMessage> _guestBookMessages = [];
  List<GuestBookMessage> get guestBookMessages => _guestBookMessages;
  // ...to here.
  1. Nella sezione di inizializzazione di ApplicationState, aggiungi le seguenti righe per sottoscrivere una query sulla raccolta di documenti quando un utente accede e annulla l'iscrizione quando si disconnette:

lib/app_state.ARROW

  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 puoi creare una query sulla raccolta guestbook e gestire l'iscrizione e la disattivazione 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'abbonamento in un secondo momento. C'è molto lavoro qui, quindi dovresti esplorarli in un debugger per capire cosa succede e ottenere un modello mentale più chiaro. Per maggiori informazioni, consulta Ricevere aggiornamenti in tempo reale con Firestore.

  1. Nel file lib/guest_book.dart, aggiungi la seguente importazione:
import 'guest_book_message.dart';
  1. Nel widget GuestBook, aggiungi un elenco di messaggi come parte della configurazione per collegare questo stato di modifica 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();
}
  1. In _GuestBookState, modifica il metodo build 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.
    );
  }
}

Aggrega i contenuti precedenti del metodo build() con un widget Column e poi aggiungi una raccolta per alla fine dei file secondari di Column per generare un nuovo Paragraph per ogni messaggio nell'elenco dei messaggi.

  1. Aggiorna il corpo di HomePage per costruire correttamente GuestBook con il nuovo parametro messages:

lib/home_page.ARROW

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:

  1. Nell'app, trova nel database i messaggi creati in precedenza.
  2. Scrivere nuovi messaggi. Vengono visualizzati immediatamente.
  3. Apri l'area di lavoro in più finestre o schede. I messaggi vengono sincronizzati in tempo reale tra le finestre e le schede.
  4. (Facoltativo) Nel menu Database della console Firebase, elimina, modifica o aggiungi manualmente nuovi messaggi. Tutte le modifiche vengono visualizzate nell'interfaccia utente.

Complimenti! Hai letto i documenti Firestore nella tua app.

Anteprima dell'app

La schermata iniziale dell&#39;app su Android con l&#39;integrazione della chat

La schermata Home dell&#39;app su iOS con integrazione della chat

La schermata Home dell&#39;app sul web con l&#39;integrazione della chat

La schermata Home dell&#39;app su macOS con l&#39;integrazione della chat

8. Configurare regole di sicurezza di base

Hai configurato inizialmente Firestore per utilizzare la modalità di test, il che significa che il database è aperto per letture e scritture. Tuttavia, ti consigliamo di utilizzare la modalità di test solo durante le prime fasi di sviluppo. Come best practice, devi impostare regole di sicurezza per il database mentre sviluppi l'app. La sicurezza è parte integrante della struttura e del comportamento della tua app.

Le regole di sicurezza di Firebase ti consentono di controllare l&#39;accesso a documenti e raccolte nel tuo database. La sintassi flessibile delle regole consente di creare regole che corrispondono a qualsiasi cosa, da tutte le scritture all'intero database alle operazioni su un documento specifico.

Configurare le regole di sicurezza di base:

  1. Nel menu Sviluppa della console Firebase, fai clic su Database > Regole. Dovresti vedere le seguenti regole di sicurezza predefinite e un avviso relativo al fatto che le regole sono pubbliche:

7767a2d2e64e7275.png

  1. 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 cerchi di scrivere sul documento abbia un UID di autenticazione corrispondente.

  1. 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&#39;accesso possono leggere i messaggi nel libro dei visitatori, ma solo l&#39;autore di un messaggio può modificarlo.

  1. Aggiungi la convalida dei dati per assicurarti che nel documento siano presenti tutti i campi previsti:
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 RSVP di un partecipante

Al momento, la tua app consente alle persone di chattare soltanto 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 fai sapere quante persone arrivano. 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 persone presenti.

  1. Nel file lib/app_state.dart, aggiungi le seguenti righe alla sezione degli accessori di ApplicationState in modo che il codice dell'interfaccia utente possa interagire con questo stato:

lib/app_state.ARROW

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

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

lib/app_state.ARROW

  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 con iscrizione 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 parteciperà.

  1. Aggiungi la seguente enumerazione all'inizio del file lib/app_state.dart.

lib/app_state.dart

enum Attending { yes, no, unknown }
  1. Crea un nuovo file yes_no_selection.dart, definisci un nuovo widget che funga da pulsanti di opzione:

lib/yes_no_selection.ARROW

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'),
              ),
            ],
          ),
        );
    }
  }
}

Inizia in uno stato di indeterminatezza se non è selezionato né No. Una volta che l'utente ha selezionato se partecipare, l'opzione viene evidenziata con un pulsante riempito e l'altra opzione si attenua con un rendering piatto.

  1. Aggiorna il metodo build() di HomePage per sfruttare YesNoSelection, consentire a un utente che ha eseguito l'accesso di indicare se parteciperà e visualizzare il numero di partecipanti all'evento:

lib/home_page.ARROW

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, pertanto i dati aggiunti con i pulsanti verranno rifiutati. Devi aggiornare le regole per consentire le aggiunte alla raccolta attendees.

  1. Nella raccolta attendees, recupera l'UID di autenticazione che hai utilizzato come nome del documento e verifica che il valore uid del mittente corrisponda al 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 l'autore può aggiornarli.

  1. Aggiungi la convalida dei dati per assicurarti che nel documento siano presenti tutti i campi previsti:
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // ... //
    match /attendees/{userId} {
      allow read: if true;
      allow write: if request.auth.uid == userId
          && "attending" in request.resource.data;

    }
  }
}
  1. (Facoltativo) Nell'app, fai clic sui pulsanti per visualizzare i risultati nella dashboard di Firestore nella Console Firebase.

Anteprima dell'app

La schermata Home dell&#39;app su Android

La schermata Home dell&#39;app su iOS

La schermata Home dell&#39;app sul web

La schermata Home dell&#39;app su macOS

10. Complimenti!

Hai utilizzato Firebase per creare un'app web interattiva e in tempo reale.

Scopri di più