Conozca Firebase para Flutter

1. Antes de comenzar

En este laboratorio de código, aprenderá algunos de los conceptos básicos de Firebase para crear aplicaciones móviles de Flutter para Android e iOS.

requisitos previos

lo que aprenderás

  • Cómo crear una aplicación de chat de libro de visitas y RSVP para eventos en Android, iOS, la Web y macOS con Flutter.
  • Cómo autenticar usuarios con Firebase Authentication y sincronizar datos con Firestore.

Lo que necesitarás

Cualquiera de los siguientes dispositivos:

  • Un dispositivo físico Android o iOS conectado a su computadora y configurado en modo desarrollador.
  • El simulador de iOS (requiere herramientas Xcode ).
  • El emulador de Android (requiere configuración en Android Studio ).

También necesitas lo siguiente:

  • Un navegador de su elección, como Google Chrome.
  • Un IDE o editor de texto de su elección configurado con los complementos Dart y Flutter, como Android Studio o Visual Studio Code .
  • La última versión stable de Flutter o beta si te gusta vivir al límite.
  • Una cuenta de Google para la creación y gestión de su proyecto Firebase.
  • Firebase CLI inició sesión en su cuenta de Google.

2. Obtenga el código de muestra

Descarga la versión inicial de tu proyecto desde GitHub:

  1. Desde la línea de comando, clona el repositorio de GitHub en el directorio flutter-codelabs :
git clone https://github.com/flutter/codelabs.git flutter-codelabs

El directorio flutter-codelabs contiene el código para una colección de codelabs. El código para este codelab está en el flutter-codelabs/firebase-get-to-know-flutter . El directorio contiene una serie de instantáneas que muestran cómo debe verse su proyecto al final de cada paso. Por ejemplo, estás en el segundo paso.

  1. Encuentre los archivos coincidentes para el segundo paso:
cd flutter-codelabs/firebase-get-to-know-flutter/step_02

Si desea saltar hacia adelante o ver cómo debería verse algo después de un paso, busque en el directorio que lleva el nombre del paso que le interesa.

Importar la aplicación de inicio

  • Abra o importe el flutter-codelabs/firebase-get-to-know-flutter/step_02 en su IDE preferido. Este directorio contiene el código de inicio para el laboratorio de código, que consiste en una aplicación de reunión de Flutter que aún no funciona.

Ubique los archivos que necesitan trabajo

El código de esta aplicación se distribuye en varios directorios. Esta división de funcionalidad facilita el trabajo porque agrupa el código por funcionalidad.

  • Localice los siguientes archivos:
    • lib/main.dart : este archivo contiene el punto de entrada principal y el widget de la aplicación.
    • lib/src/widgets.dart : este archivo contiene varios widgets para ayudar a estandarizar el estilo de la aplicación. Componen la pantalla de la aplicación de inicio.
    • lib/src/authentication.dart : este archivo contiene una implementación parcial de la autenticación con un conjunto de widgets para crear una experiencia de usuario de inicio de sesión para la autenticación basada en correo electrónico de Firebase. Estos widgets para el flujo de autenticación aún no se usan en la aplicación de inicio, pero los agregará pronto.

Agrega archivos adicionales según sea necesario para compilar el resto de la aplicación.

Revise el archivo lib/main.dart

Esta aplicación aprovecha el paquete google_fonts para hacer que Roboto sea la fuente predeterminada en toda la aplicación. Puede explorar fonts.google.com y usar las fuentes que descubra allí en diferentes partes de la aplicación.

Utiliza los widgets auxiliares del archivo lib/src/widgets.dart en forma de Header , Paragraph e icono y IconAndDetail . Estos widgets eliminan el código duplicado para reducir el desorden en el diseño de la página descrito en HomePage . Esto también permite una apariencia y sensación consistentes.

Así es como se ve su aplicación en Android, iOS, la Web y macOS:

3. Crea y configura un proyecto de Firebase

La visualización de la información del evento es excelente para sus invitados, pero no es muy útil para nadie por sí sola. Debe agregar alguna funcionalidad dinámica a la aplicación. Para hacerlo, debe conectar Firebase a su aplicación. Para comenzar con Firebase, debe crear y configurar un proyecto de Firebase.

Crear un proyecto de Firebase

  1. Inicie sesión en Firebase .
  2. En la consola, haga clic en Agregar proyecto o Crear un proyecto .
  3. En el campo Nombre del proyecto , ingresa Firebase-Flutter-Codelab y luego haz clic en Continuar .

4395e4e67c08043a.png

  1. Haga clic en las opciones de creación de proyectos. Si se le solicita, acepte los términos de Firebase, pero omita la configuración de Google Analytics porque no la usará para esta aplicación.

b7138cde5f2c7b61.png

Para obtener más información sobre los proyectos de Firebase, consulte Comprender los proyectos de Firebase .

La aplicación utiliza los siguientes productos de Firebase, que están disponibles para aplicaciones web:

  • Autenticación: permite a los usuarios iniciar sesión en su aplicación.
  • Firestore: guarda datos estructurados en la nube y recibe notificaciones instantáneas cuando cambian los datos.
  • Reglas de seguridad de Firebase: protege su base de datos.

Algunos de estos productos necesitan una configuración especial o debe habilitarlos en la consola de Firebase.

Habilitar la autenticación de inicio de sesión de correo electrónico

  1. En el panel de descripción general del proyecto de Firebase console, expanda el menú Build .
  2. Haga clic en Autenticación > Comenzar > Método de inicio de sesión > Correo electrónico/Contraseña > Habilitar > Guardar .

58e3e3e23c2f16a4.png

Habilitar Firestore

La aplicación web usa Firestore para guardar mensajes de chat y recibir nuevos mensajes de chat.

Habilitar Firestore:

  • En el menú Generar , haga clic en Cloud Firestore > Crear base de datos .

99e8429832d23fa3.png

  1. Seleccione Iniciar en modo de prueba y luego lea el descargo de responsabilidad sobre las reglas de seguridad. El modo de prueba garantiza que pueda escribir libremente en la base de datos durante el desarrollo.

6be00e26c72ea032.png

  1. Haga clic en Siguiente y luego seleccione la ubicación de su base de datos. Puede utilizar el valor predeterminado. No puedes cambiar la ubicación más tarde.

278656eefcfb0216.png

  1. Haga clic en Habilitar .

4. Configurar base de fuego

Para usar Firebase con Flutter, debe completar las siguientes tareas para configurar el proyecto Flutter para usar las bibliotecas FlutterFire correctamente:

  1. Agregue las dependencias de FlutterFire a su proyecto.
  2. Registre la plataforma deseada en el proyecto Firebase.
  3. Descargue el archivo de configuración específico de la plataforma y luego agréguelo al código.

En el directorio de nivel superior de su aplicación Flutter, hay subdirectorios android , ios , macos y web , que contienen los archivos de configuración específicos de la plataforma para iOS y Android, respectivamente.

Configurar dependencias

Debe agregar las bibliotecas de FlutterFire para los dos productos de Firebase que usa en esta aplicación: Autenticación y Firestore.

  • Desde la línea de comando, agregue las siguientes dependencias:
$ flutter pub add firebase_core

El paquete firebase_core es el código común requerido para todos los complementos de Firebase Flutter.

$ flutter pub add firebase_auth

El paquete firebase_auth permite la integración con la autenticación.

$ flutter pub add cloud_firestore

El paquete cloud_firestore permite el acceso al almacenamiento de datos de Firestore.

$ flutter pub add provider

El paquete firebase_ui_auth proporciona un conjunto de widgets y utilidades para aumentar la velocidad del desarrollador con flujos de autenticación.

$ flutter pub add firebase_ui_auth

Agregó los paquetes requeridos, pero también necesita configurar los proyectos iOS, Android, macOS y Web runner para usar Firebase de manera apropiada. También utiliza el paquete de provider que permite la separación de la lógica empresarial de la lógica de visualización.

Instale la CLI de FlutterFire

La CLI de FlutterFire depende de la CLI de Firebase subyacente.

  1. Si aún no lo ha hecho, instale Firebase CLI en su máquina.
  2. Instale la CLI de FlutterFire:
$ dart pub global activate flutterfire_cli

Una vez instalado, el comando flutterfire está disponible globalmente.

Configura tus aplicaciones

La CLI extrae información de su proyecto Firebase y aplicaciones de proyectos seleccionados para generar toda la configuración para una plataforma específica.

En la raíz de su aplicación, ejecute el comando de configure :

$ flutterfire configure

El comando de configuración lo guía a través de los siguientes procesos:

  1. Seleccione un proyecto de Firebase basado en el archivo .firebaserc o desde Firebase Console.
  2. Determine las plataformas para la configuración, como Android, iOS, macOS y web.
  3. Identifique las aplicaciones de Firebase desde las que extraer la configuración. De manera predeterminada, la CLI intenta hacer coincidir automáticamente las aplicaciones de Firebase según la configuración actual de su proyecto.
  4. Genere un archivo firebase_options.dart en su proyecto.

Configurar macOS

Flutter en macOS crea aplicaciones completamente protegidas. Como esta aplicación se integra con la red para comunicarse con los servidores de Firebase, debe configurar su aplicación con privilegios de cliente de red.

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>

Para obtener más información, consulte Soporte de escritorio para Flutter .

5. Agregue la funcionalidad RSVP

Ahora que agregó Firebase a la aplicación, puede crear un botón RSVP que registra a las personas con Autenticación . Para Android nativo, iOS nativo y Web, existen paquetes FirebaseUI Auth prediseñados, pero debe crear esta capacidad para Flutter.

El proyecto que recuperó anteriormente incluía un conjunto de widgets que implementa la interfaz de usuario para la mayor parte del flujo de autenticación. Implementa la lógica comercial para integrar la autenticación con la aplicación.

Agregar lógica de negocios con el paquete de Provider

Use el paquete del provider para hacer que un objeto de estado de aplicación centralizado esté disponible en todo el árbol de widgets de Flutter de la aplicación:

  1. Modifique las importaciones en la parte superior del archivo lib/main.dart :

lib/principal.dardo

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

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

Las declaraciones de import presentan Firebase Core y Auth, extraen el paquete del provider que hace que el objeto de estado de la aplicación esté disponible en todo el árbol de widgets e incluyen los widgets de autenticación del paquete firebase_ui_auth .

Este objeto de estado de la aplicación ApplicationState tiene una responsabilidad principal en este paso, que es alertar al árbol de widgets de que hubo una actualización de un estado autenticado.

  1. Agregue la siguiente clase al final del archivo lib/main.dart :

lib/principal.dardo

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();
    });
  }
}

Solo utiliza un proveedor para comunicar el estado de inicio de sesión de un usuario a la aplicación. Para permitir que un usuario inicie sesión, use las IU proporcionadas por el paquete firebase_ui_auth , que es una excelente manera de iniciar rápidamente las pantallas de inicio de sesión en sus aplicaciones.

Integrar el flujo de autenticación

  1. Conecte el estado de la aplicación con la inicialización de la aplicación y luego agregue el flujo de autenticación a la página de HomePage :

lib/principal.dardo

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

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

La modificación de la función main() hace que el paquete del proveedor sea responsable de la instanciación del objeto de estado de la aplicación con el widget ChangeNotifierProvider . Utiliza esta clase provider específica porque el objeto de estado de la aplicación amplía la clase ChangeNotifier , lo que permite que el paquete del provider sepa cuándo volver a mostrar los widgets dependientes.

  1. Actualice su aplicación para manejar la navegación a diferentes pantallas que FirebaseUI le proporciona:

lib/principal.dardo

class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      //Start adding here
      initialRoute: '/home',
      routes: {
        '/home': (context) {
          return const HomePage();
        },
        '/sign-in': ((context) {
          return SignInScreen(
            actions: [
              ForgotPasswordAction(((context, email) {
                Navigator.of(context)
                    .pushNamed('/forgot-password', arguments: {'email': email});
              })),
              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);
                  }
                  Navigator.of(context).pushReplacementNamed('/home');
                }
              })),
            ],
          );
        }),
        '/forgot-password': ((context) {
          final arguments = ModalRoute.of(context)?.settings.arguments
              as Map<String, dynamic>?;

          return ForgotPasswordScreen(
            email: arguments?['email'] as String,
            headerMaxExtent: 200,
          );
        }),
        '/profile': ((context) {
          return ProfileScreen(
            providers: [],
            actions: [
              SignedOutAction(
                ((context) {
                  Navigator.of(context).pushReplacementNamed('/home');
                }),
              ),
            ],
          );
        })
      },
      // end adding here
      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,
      ),
    );
  }
}

Cada pantalla tiene un tipo diferente de acción asociada con ella en función del nuevo estado del flujo de autenticación. Después de la mayoría de los cambios de estado en la autenticación, puede volver a enrutar a una pantalla preferida, ya sea la pantalla de inicio o una pantalla diferente, como el perfil.

  1. En el método de compilación de la clase HomePage , integre el estado de la aplicación con el widget AuthFunc :

lib/principal.dardo

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Consumer<ApplicationState>(
        builder: (context, appState, child) => 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!',
                  ),
                ],
              ),
            ));
  }
}

Crea una instancia del widget AuthFunc y lo envuelve en un widget de Consumer . El widget del consumidor es la forma habitual en que el paquete del provider se puede usar para reconstruir parte del árbol cuando cambia el estado de la aplicación. El widget AuthFunc son los widgets complementarios que prueba.

Probar el flujo de autenticación

cdf2d25e436bd48d.png

  1. En la aplicación, toque el botón RSVP para iniciar SignInScreen .

2a2cd6d69d172369.png

  1. Introduzca una dirección de correo electrónico. Si ya está registrado, el sistema le solicita que ingrese una contraseña. De lo contrario, el sistema le solicita que complete el formulario de registro.

e5e65065dba36b54.png

  1. Ingrese una contraseña de menos de seis caracteres para verificar el flujo de manejo de errores. Si está registrado, verá la contraseña en su lugar.
  2. Introduzca contraseñas incorrectas para comprobar el flujo de gestión de errores.
  3. Introduzca la contraseña correcta. Verá la experiencia de inicio de sesión, que ofrece al usuario la posibilidad de cerrar sesión.

4ed811a25b0cf816.png

6. Escribir mensajes a Firestore

Es genial saber que los usuarios van a venir, pero debes darles a los invitados algo más que hacer en la aplicación. ¿Y si pudieran dejar mensajes en un libro de visitas? Pueden compartir por qué están emocionados de venir oa quién esperan conocer.

Para almacenar los mensajes de chat que los usuarios escriben en la aplicación, utiliza Firestore .

Modelo de datos

Firestore es una base de datos NoSQL y los datos almacenados en la base de datos se dividen en colecciones, documentos, campos y subcolecciones. Almacena cada mensaje del chat como un documento en una colección de gustbook de visitas, que es una colección de nivel superior.

7c20dc8424bb1d84.png

Agregar mensajes a Firestore

En esta sección, agrega la funcionalidad para que los usuarios escriban mensajes en la base de datos. Primero, agrega un campo de formulario y un botón de envío, y luego agrega el código que conecta estos elementos con la base de datos.

  1. En el archivo lib/main.dart , agregue importaciones para los cloud_firestore y dart:async :

lib/principal.dardo

import 'dart:async';                                    // new

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

import 'firebase_options.dart';
import 'src/authentication.dart';
import 'src/widgets.dart';
  1. Al final del archivo lib/main.dart , agregue un widget con estado de GuestBook para construir los elementos de la interfaz de usuario de un campo de mensaje y un botón de envío:

lib/principal.dardo

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

Hay un par de puntos de interés aquí. Primero, crea una instancia de un formulario para que pueda validar que el mensaje realmente contiene contenido y mostrarle al usuario un mensaje de error si no hay ninguno. Para validar un formulario, accede al estado del formulario detrás del formulario con una GlobalKey . Para obtener más información sobre las claves y cómo usarlas, consulte Cuándo usar las claves .

También tenga en cuenta la forma en que se distribuyen los widgets, tiene una Row con un TextFormField y un StyledButton , que contiene una Row . También tenga en cuenta que TextFormField está envuelto en un widget Expanded , lo que obliga a TextFormField a llenar cualquier espacio adicional en la fila. Para comprender mejor por qué esto es necesario, consulte Descripción de las restricciones .

Ahora que tiene un widget que le permite al usuario ingresar texto para agregarlo al Libro de invitados, debe mostrarlo en la pantalla.

  1. Edite el cuerpo de HomePage para agregar las siguientes dos líneas al final de los elementos secundarios de ListView :
const Header("What we'll be doing"),
const Paragraph(
  'Join us for a day full of Firebase Workshops and Pizza!',
),
// Add the following two lines.
const Header('Discussion'),
GuestBook(addMessage: (message) => print(message)),

Si bien esto es suficiente para mostrar el widget, no es suficiente para hacer nada útil. Actualice este código en breve para que sea funcional.

Vista previa de la aplicación

Cuando un usuario hace clic en ENVIAR , activa el siguiente fragmento de código. Agrega el contenido del campo de entrada del mensaje a la colección del libro de guestbook de la base de datos. Específicamente, el método addMessageToGuestBook agrega el contenido del mensaje a un nuevo documento con una identificación generada automáticamente en la colección del libro de guestbook .

Tenga en cuenta que FirebaseAuth.instance.currentUser.uid es una referencia a la ID única generada automáticamente que proporciona la autenticación para todos los usuarios que iniciaron sesión.

  • En el archivo lib/main.dart , agregue el método addMessageToGuestBook . Conecta esta capacidad con la interfaz de usuario en el siguiente paso.

lib/principal.dardo

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

Conecte la interfaz de usuario y la base de datos

Tiene una interfaz de usuario donde el usuario puede ingresar el texto que desea agregar al Libro de invitados y tiene el código para agregar la entrada a Firestore. Ahora todo lo que necesita hacer es conectar los dos.

  • En el archivo lib/main.dart , realice el siguiente cambio en el widget HomePage :

lib/principal.dardo

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

Reemplazó las dos líneas que agregó al comienzo de este paso con la implementación completa. Vuelve a usar Consumer<ApplicationState> para que el estado de la aplicación esté disponible para la parte del árbol que representas. Esto le permite reaccionar ante alguien que ingresa un mensaje en la interfaz de usuario y publicarlo en la base de datos. En la siguiente sección, prueba si los mensajes agregados se publican en la base de datos.

Probar el envío de mensajes

  1. Si es necesario, inicie sesión en la aplicación.
  2. Introduzca un mensaje, como Hey there! y luego haga clic en ENVIAR .

Esta acción escribe el mensaje en su base de datos de Firestore. Sin embargo, no ve el mensaje en su aplicación Flutter real porque aún necesita implementar la recuperación de los datos, lo que hace en el siguiente paso. Sin embargo, en el panel de control de la base de datos de la consola Firebase, puede ver su mensaje agregado en la colección del libro de guestbook . Si envía más mensajes, agrega más documentos a su colección de libros de guestbook . Por ejemplo, vea el siguiente fragmento de código:

713870af0b3b63c.png

7. Leer mensajes

Es genial que los invitados puedan escribir mensajes en la base de datos, pero aún no pueden verlos en la aplicación. ¡Es hora de arreglar eso!

Sincronizar mensajes

Para mostrar mensajes, debe agregar oyentes que se activen cuando cambien los datos y luego crear un elemento de interfaz de usuario que muestre nuevos mensajes. Agrega código al estado de la aplicación que escucha los mensajes recién agregados de la aplicación.

  1. En el archivo lib/main.dart antes del widget GuestBook , agregue la siguiente clase para exponer una vista estructurada de los datos que almacena en Firestore.

lib/principal.dardo

class GuestBookMessage {
  GuestBookMessage({required this.name, required this.message});
  final String name;
  final String message;
}
  1. En la sección de ApplicationState donde define el estado y los captadores, agregue las siguientes líneas:

lib/principal.dardo

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

  // Add from here...
  StreamSubscription<QuerySnapshot>? _guestBookSubscription;
  List<GuestBookMessage> _guestBookMessages = [];
  List<GuestBookMessage> get guestBookMessages => _guestBookMessages;
  // ...to here.
  1. En la sección de inicialización de ApplicationState , agregue las siguientes líneas para suscribirse a una consulta sobre la colección de documentos cuando un usuario inicia sesión y cancelar la suscripción cuando cierra la sesión:

lib/principal.dardo

  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();
    });
  }

Esta sección es importante porque es donde construye una consulta sobre la colección del libro de guestbook y maneja la suscripción y cancelación de suscripción a esta colección. Usted escucha la transmisión, donde reconstruye un caché local de los mensajes en la colección del libro de guestbook y también almacena una referencia a esta suscripción para que pueda darse de baja más tarde. Están sucediendo muchas cosas aquí, por lo que debe explorarlo en un depurador para inspeccionar lo que sucede para obtener un modelo mental más claro. Para obtener más información, consulte Obtenga actualizaciones en tiempo real con Firestore .

  1. En el widget GuestBook , agregue una lista de mensajes como parte de la configuración para conectar este estado cambiante a la interfaz de usuario:

lib/principal.dardo

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. En _GuestBookState , modifique el método de build de la siguiente manera para exponer esta configuración:

lib/principal.dardo

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

Envuelve el contenido anterior del método build() con un widget de Column y luego agrega una colección al final de los elementos secundarios de Column para generar un nuevo Paragraph para cada mensaje en la lista de mensajes.

  1. Actualice el cuerpo de HomePage para construir correctamente GuestBook con el nuevo parámetro de messages :

lib/principal.dardo

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
        ),
      ],
    ],
  ),
),

Sincronización de mensajes de prueba

Firestore sincroniza de forma automática e instantánea los datos con los clientes suscritos a la base de datos.

Prueba de sincronización de mensajes:

  1. En la aplicación, busque los mensajes que creó anteriormente en la base de datos.
  2. Escribe nuevos mensajes. Aparecen al instante.
  3. Abre tu espacio de trabajo en múltiples ventanas o pestañas. Los mensajes se sincronizan en tiempo real en las ventanas y pestañas.
  4. Opcional: en el menú Base de datos de la consola Firebase, elimine, modifique o agregue nuevos mensajes manualmente. Todos los cambios aparecen en la interfaz de usuario.

¡Felicidades! ¡Lees documentos de Firestore en tu aplicación!

Vista previa de la aplicación

8. Establezca reglas básicas de seguridad

Inicialmente configuró Firestore para usar el modo de prueba, lo que significa que su base de datos está abierta para lecturas y escrituras. Sin embargo, solo debe usar el modo de prueba durante las primeras etapas de desarrollo. Como práctica recomendada, debe configurar reglas de seguridad para su base de datos a medida que desarrolla su aplicación. La seguridad es parte integral de la estructura y el comportamiento de su aplicación.

Las reglas de seguridad de Firebase le permiten controlar el acceso a documentos y colecciones en su base de datos. La sintaxis de reglas flexibles le permite crear reglas que coincidan con cualquier cosa, desde todas las escrituras en toda la base de datos hasta operaciones en un documento específico.

Configure reglas básicas de seguridad:

  1. En el menú Desarrollar de la consola Firebase, haga clic en Base de datos > Reglas . Debería ver las siguientes reglas de seguridad predeterminadas y una advertencia acerca de que las reglas son públicas:

7767a2d2e64e7275.png

  1. Identifique las colecciones en las que la aplicación escribe datos:

En match /databases/{database}/documents , identifique la colección que desea proteger:

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

Debido a que usó el UID de autenticación como un campo en cada documento del libro de visitas, puede obtener el UID de autenticación y verificar que cualquier persona que intente escribir en el documento tenga un UID de autenticación coincidente.

  1. Agregue las reglas de lectura y escritura a su conjunto de reglas:
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;
    }
  }
}

Ahora, solo los usuarios registrados pueden leer mensajes en el libro de visitas, pero solo el autor de un mensaje puede editar un mensaje.

  1. Agregue validación de datos para garantizar que todos los campos esperados estén presentes en el 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. Paso de bonificación: practica lo que has aprendido

Registrar el estado de RSVP de un asistente

En este momento, su aplicación solo permite que las personas chateen cuando estén interesadas en el evento. Además, la única forma de saber si alguien va a venir es cuando lo dice en el chat.

En este paso, te organizas y le dices a la gente cuántas personas van a venir. Agrega un par de capacidades al estado de la aplicación. La primera es la posibilidad de que un usuario que haya iniciado sesión indique si asistirá. El segundo es un contador de cuántas personas están asistiendo.

  1. En el archivo lib/main.dart , agregue las siguientes líneas a la sección de accesores para que el código de la interfaz de usuario pueda interactuar con este estado:

lib/principal.dardo

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. Actualice el método init() de ApplicationState de la siguiente manera:

lib/principal.dardo

  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();
    });
  }

Este código agrega una consulta siempre suscrita para determinar la cantidad de asistentes y una segunda consulta que solo está activa mientras un usuario inicia sesión para determinar si asistirá.

  1. Agregue la siguiente enumeración después de la declaración GuestBookMessage :

lib/principal.dardo

enum Attending { yes, no, unknown }
  1. Defina un nuevo widget que actúe como los botones de radio de antaño:

lib/principal.dardo

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

Comienza en un estado indeterminado sin seleccionar ni No. Una vez que el usuario selecciona si asistirá, muestra esa opción resaltada con un botón relleno y la otra opción retrocede con una representación plana.

  1. Actualice el método build() de HomePage para aprovechar YesNoSelection , permita que un usuario que haya iniciado sesión indique si asistirá y muestre la cantidad de asistentes al evento:

lib/principal.dardo

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,
        ),
      ],
    ],
  ),
),

Agregar reglas

Ya configuró algunas reglas, por lo que los datos que agregue con los botones serán rechazados. Debe actualizar las reglas para permitir adiciones a la colección de attendees .

  1. En la colección de attendees , tome el UID de autenticación que utilizó como nombre del documento y verifique que el UID del uid sea el mismo que el del documento que está escribiendo:
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // ... //
    match /attendees/{userId} {
      allow read: if true;
      allow write: if request.auth.uid == userId;
    }
  }
}

Esto permite que todos lean la lista de asistentes porque no hay datos privados allí, pero solo el creador puede actualizarla.

  1. Agregue validación de datos para garantizar que todos los campos esperados estén presentes en el 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;

    }
  }
}
  1. Opcional: en la aplicación, haga clic en los botones para ver los resultados en el panel de control de Firestore en la consola de Firebase.

Vista previa de la aplicación

10. ¡Felicitaciones!

¡Utilizó Firebase para crear una aplicación web interactiva en tiempo real!

Aprende más