Descubre Firebase para Flutter

1. Antes de comenzar

En este codelab, aprenderás algunos de los conceptos básicos de Firebase a fin de crear apps de Flutter para dispositivos móviles en iOS y Android.

Requisitos previos

Qué aprenderás

  • Cómo compilar una app de chat para confirmar asistencia y hacer un libro de visitas a un evento en Android, iOS, la Web y macOS con Flutter.
  • Cómo autenticar usuarios con Firebase Authentication y sincronizar datos con Firestore.

La pantalla principal de la app en Android

La pantalla principal de la app en iOS

Requisitos

Cualquiera de los siguientes dispositivos:

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

También necesitas lo siguiente:

  • El navegador que prefieras, como Google Chrome
  • Un IDE o editor de texto que prefieras, configurado con los complementos de Dart y Flutter, como Android Studio o Visual Studio Code
  • La versión stable más reciente de Flutter o beta, si te gusta vivir al límite
  • Una Cuenta de Google para la creación y administración de tu proyecto de Firebase
  • La CLI de Firebase accedió a tu Cuenta de Google.

2. Obtén el código de muestra

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

  1. Desde la línea de comandos, 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 de una colección de codelabs. El código de este codelab se encuentra en el directorio flutter-codelabs/firebase-get-to-know-flutter. El directorio contiene una serie de instantáneas que muestran cómo debería verse tu proyecto al final de cada paso. Por ejemplo, estás en el segundo paso.

  1. Busca los archivos que coincidan para el segundo paso:
cd flutter-codelabs/firebase-get-to-know-flutter/step_02

Si deseas avanzar o ver cómo debería verse algo después de un paso, busca en el directorio que tiene el nombre del paso que te interesa.

Importa la app de partida

  • Abre o importa el directorio flutter-codelabs/firebase-get-to-know-flutter/step_02 en tu IDE preferido. Este directorio contiene el código de partida para el codelab, que consiste en una app de reunión de Flutter que aún no es funcional.

Ubica los archivos que necesitan trabajo

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

  • Ubica los siguientes archivos:
    • lib/main.dart: Este archivo contiene el punto de entrada principal y el widget de la app.
    • lib/home_page.dart: Este archivo contiene el widget de la página principal.
    • lib/src/widgets.dart: Este archivo contiene varios widgets que ayudan a estandarizar el estilo de la app. Estos elementos componen la pantalla de la app de partida.
    • lib/src/authentication.dart: Este archivo contiene una implementación parcial de Authentication con un conjunto de widgets para crear una experiencia del usuario de acceso para la autenticación basada en correo electrónico de Firebase. Estos widgets para el flujo de Auth aún no se usan en la app de partida, pero los agregarás pronto.

Agrega los archivos adicionales que sean necesarios para compilar el resto de la app.

Revisa el archivo lib/main.dart

Esta app aprovecha el paquete google_fonts para hacer que Roboto sea la fuente predeterminada en toda la app. Puedes explorar fonts.google.com y usar las fuentes que descubras en diferentes partes de la app.

Usa los widgets auxiliares del archivo lib/src/widgets.dart en forma de Header, Paragraph 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 un estilo coherente.

La app se verá de la siguiente manera en Android, iOS, la Web y macOS:

La pantalla principal de la app en Android

La pantalla principal de la app en iOS

La pantalla principal de la app en la Web

La pantalla principal de la app en macOS

3. Crea y configura un proyecto de Firebase

La visualización de la información de los eventos es excelente para los invitados, pero no es muy útil para nadie por sí solo. Debes agregar alguna funcionalidad dinámica a la app. Para ello, debes conectar Firebase a tu app. Para comenzar a usar Firebase, debes crear y configurar un proyecto de Firebase.

Crea un proyecto de Firebase

  1. Accede a Firebase.
  2. En la consola, haz 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. Haz clic para avanzar por las opciones de creación del proyecto. Si se te solicita, acepta las condiciones de Firebase, pero omite la configuración de Google Analytics porque no lo utilizarás para esta aplicación.

b7138cde5f2c7b61.png

Para obtener más información sobre los proyectos de Firebase, consulta Información sobre los proyectos de Firebase.

La app usa los siguientes productos de Firebase, que están disponibles para apps web:

  • Authentication: Permite que los usuarios accedan a tu app.
  • Firestore: Guarda los datos estructurados en la nube y recibe notificaciones instantáneas cuando cambian los datos.
  • Reglas de seguridad de Firebase: Protegen tu base de datos.

Algunos de estos productos necesitan una configuración especial o debes habilitarlos en Firebase console.

Habilitar la autenticación de acceso con correo electrónico

  1. En el panel Descripción general del proyecto de Firebase console, expande el menú Compilación.
  2. Haz clic en Authentication > Get Started > Sign-in method > Email/Password > Enable > Save.

58e3e3e23c2f16a4.png

Habilita Firestore

La app web usa Firestore para guardar mensajes de chat y recibir mensajes nuevos.

Habilita Firestore:

  • En el menú Compilación, haz clic en Base de datos de Firestore > Crear base de datos.

99e8429832d23fa3.png

  1. Selecciona Comenzar en modo de prueba y, luego, lee la renuncia de responsabilidad sobre las reglas de seguridad. El modo de prueba garantiza que puedas escribir con libertad en la base de datos durante el desarrollo.

6be00e26c72ea032.png

  1. Haz clic en Siguiente y, luego, selecciona la ubicación de la base de datos. Puedes usar la opción predeterminada. No puedes cambiar la ubicación más adelante.

278656eefcfb0216.png

  1. Haz clic en Habilitar.

4. Configura Firebase

Para usar Firebase con Flutter, debes completar las siguientes tareas para configurar el proyecto de Flutter de modo que use las bibliotecas FlutterFire de forma correcta:

  1. Agrega las dependencias FlutterFire a tu proyecto.
  2. Registra la plataforma deseada en el proyecto de Firebase.
  3. Descarga el archivo de configuración específico de la plataforma y, luego, agrégalo al código.

En el directorio de nivel superior de tu app de Flutter, encontrarás los subdirectorios android, ios, macos y web, que contienen los archivos de configuración específicos de la plataforma para iOS y Android, respectivamente.

Cómo configurar dependencias

Debes agregar las bibliotecas FlutterFire para los dos productos de Firebase que usas en esta app: Authentication y Firestore.

  • Desde la línea de comandos, agrega 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 para Flutter.

$ flutter pub add firebase_auth

El paquete firebase_auth permite la integración con Authentication.

$ flutter pub add cloud_firestore

El paquete cloud_firestore habilita 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

Agregaste los paquetes necesarios, pero también debes configurar los proyectos de iOS, Android, macOS y Web Runner para usar Firebase de forma adecuada. También puedes usar el paquete provider, que permite separar la lógica empresarial de la lógica de visualización.

Instala la CLI de FlutterFire

La CLI de FlutterFire depende de la Firebase CLI subyacente.

  1. Si aún no lo hiciste, instala Firebase CLI en tu máquina.
  2. Instala la CLI de FlutterFire:
$ dart pub global activate flutterfire_cli

Una vez instalado, el comando flutterfire está disponible de manera global.

Configura tus apps

La CLI extrae información de tu proyecto de Firebase y de las apps del proyecto seleccionado para generar toda la configuración de una plataforma específica.

En la raíz de tu app, ejecuta el comando configure:

$ flutterfire configure

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

  1. Selecciona un proyecto de Firebase según el archivo .firebaserc o desde Firebase console.
  2. Determina plataformas para la configuración, como Android, iOS, macOS y la Web.
  3. Identifica las apps de Firebase de las que se debe extraer la configuración. De forma predeterminada, la CLI intenta hacer coincidir automáticamente las apps de Firebase según la configuración actual de tu proyecto.
  4. Genera un archivo firebase_options.dart en tu proyecto.

Configura macOS

Flutter en macOS compila apps de zona de pruebas completamente. Debido a que esta app se integra con la red para comunicarse con los servidores de Firebase, debes configurar tu app 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, consulta Compatibilidad con Flutter en computadoras de escritorio.

5. Agrega la función para confirmar asistencia

Ahora que agregaste Firebase a la app, puedes crear un botón Confirmar asistencia que registre a las personas con Authentication. Para los nativos de Android, iOS y la Web, existen paquetes FirebaseUI Auth precompilados, pero debes compilar esta capacidad para Flutter.

El proyecto que recuperaste antes incluía un conjunto de widgets que implementa la interfaz de usuario para la mayor parte del flujo de autenticación. Implementarás la lógica empresarial para integrar Authentication con la app.

Agrega la lógica empresarial con el paquete Provider

Usa el paquete provider para hacer que un objeto de estado centralizado de la app esté disponible en todo el árbol de los widgets de Flutter de la app:

  1. Crea un archivo nuevo llamado app_state.dart con el siguiente contenido:

lib/app_state.dart

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

import 'firebase_options.dart';

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

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

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

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

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

Las sentencias import presentan Firebase Core y Auth, extraen el paquete provider que hace que el objeto de estado de la app 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 que hubo una actualización de un estado autenticado.

Solo debes usar un proveedor para comunicar el estado del estado de acceso de un usuario a la app. Para permitir que un usuario acceda, usa las IU que proporciona el paquete firebase_ui_auth, que son una excelente manera de iniciar rápidamente las pantallas de acceso en tus apps.

Integra el flujo de autenticación

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

lib/main.dart

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

import 'app_state.dart';                                 // new
import 'home_page.dart';
  1. Conecta el estado de la app con su inicialización y, luego, agrega el flujo de autenticación 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 modificación de la función main() hace que el paquete del proveedor sea responsable de la creación de instancias del objeto de estado de la app con el widget ChangeNotifierProvider. Usarás esta clase provider específica porque el objeto de estado de la app extiende la clase ChangeNotifier, lo que le permite al paquete provider saber cuándo volver a mostrar widgets dependientes.

  1. Crea una configuración GoRouter para actualizar tu app a fin de controlar la navegación a diferentes pantallas que te proporciona FirebaseUI:

lib/main.dart

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

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

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

Cada pantalla tiene un tipo diferente de acción asociado 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, puedes volver a la pantalla preferida, ya sea la pantalla principal o una diferente, como el perfil.

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

lib/home_page.dart

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

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

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Firebase Meetup'),
      ),
      body: ListView(
        children: <Widget>[
          Image.asset('assets/codelab.png'),
          const SizedBox(height: 8),
          const IconAndDetail(Icons.calendar_today, 'October 30'),
          const IconAndDetail(Icons.location_city, 'San Francisco'),
          // Add from here
          Consumer<ApplicationState>(
            builder: (context, appState, _) => AuthFunc(
                loggedIn: appState.loggedIn,
                signOut: () {
                  FirebaseAuth.instance.signOut();
                }),
          ),
          // to here
          const Divider(
            height: 8,
            thickness: 1,
            indent: 8,
            endIndent: 8,
            color: Colors.grey,
          ),
          const Header("What we'll be doing"),
          const Paragraph(
            'Join us for a day full of Firebase Workshops and Pizza!',
          ),
        ],
      ),
    );
  }
}

Crea una instancia del widget AuthFunc y únelo a un widget Consumer. El widget de consumidor es la forma habitual en la que se puede usar el paquete provider para volver a compilar parte del árbol cuando cambia el estado de la app. El widget AuthFunc son los widgets complementarios que pruebas.

Prueba el flujo de autenticación

cdf2d25e436bd48d.png

  1. En la app, presiona el botón RSVP para iniciar el SignInScreen.

2a2cd6d69d172369.png

  1. Ingrese una dirección de correo electrónico. Si ya te registraste, el sistema te pedirá que ingreses una contraseña. De lo contrario, el sistema te solicitará que completes el formulario de registro.

e5e65065dba36b54.png

  1. Ingresa una contraseña que tenga menos de seis caracteres para verificar el flujo de manejo de errores. Si ya te registraste, verás la contraseña correspondiente.
  2. Ingresa contraseñas incorrectas para verificar el flujo de manejo de errores.
  3. Ingresa la contraseña correcta. Verás la experiencia de acceso, que ofrece al usuario la posibilidad de salir.

4ed811a25b0cf816.png

6. Escribe mensajes en Firestore

Es genial saber que los usuarios vienen, pero debes brindarles a los invitados otra actividad en la app. ¿Y si pudieran dejar mensajes en un libro de visitas? Pueden compartir por qué están entusiasmados por venir o a quién esperan conocer.

Para almacenar los mensajes de chat que los usuarios escriben en la app, usa Firestore.

Modelo de datos

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

8c20dc8424bb1d84.png

Agregue mensajes a Firestore

En esta sección, agregarás la funcionalidad para que los usuarios escriban mensajes en la base de datos. Primero, agregas un campo de formulario y un botón para enviar y, luego, agregas el código que conecta estos elementos con la base de datos.

  1. Crea un archivo nuevo llamado guest_book.dart, agrega un widget con estado GuestBook para construir los elementos de la IU de un campo de mensaje y un botón de envío:

lib/guest_book.dart

import 'dart:async';

import 'package:flutter/material.dart';

import 'src/widgets.dart';

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

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

  @override
  State<GuestBook> createState() => _GuestBookState();
}

class _GuestBookState extends State<GuestBook> {
  final _formKey = GlobalKey<FormState>(debugLabel: '_GuestBookState');
  final _controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Form(
        key: _formKey,
        child: Row(
          children: [
            Expanded(
              child: TextFormField(
                controller: _controller,
                decoration: const InputDecoration(
                  hintText: 'Leave a message',
                ),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Enter your message to continue';
                  }
                  return null;
                },
              ),
            ),
            const SizedBox(width: 8),
            StyledButton(
              onPressed: () async {
                if (_formKey.currentState!.validate()) {
                  await widget.addMessage(_controller.text);
                  _controller.clear();
                }
              },
              child: Row(
                children: const [
                  Icon(Icons.send),
                  SizedBox(width: 4),
                  Text('SEND'),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Aquí hay algunos lugares de interés. Primero, crea una instancia de un formulario para validar que el mensaje realmente incluya contenido y mostrarle al usuario un mensaje de error, si no lo hay. Para validar un formulario, accede al estado que se encuentra detrás del formulario con un GlobalKey. Para obtener más información sobre las claves y su uso, consulta Cuándo usar claves.

También observa la forma en que se presentan los widgets, tienes un Row con un TextFormField y un StyledButton, que contiene un Row. Además, ten en cuenta que TextFormField se une a un widget Expanded, lo que fuerza a TextFormField a llenar cualquier espacio adicional en la fila. Para comprender mejor por qué esto es necesario, consulta Información sobre las restricciones.

Ahora que tienes un widget que permite al usuario ingresar texto para agregar al libro de invitados, debes verlo en la pantalla.

  1. Edita 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. Pronto actualizarás este código para que funcione.

Vista previa de la app

La pantalla principal de la app en Android con integración de chat

La pantalla principal de la app en iOS con integración de chat

La pantalla principal de la app en la Web con integración de chat

La pantalla principal de la app en macOS con integración de chat

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

Ten en cuenta que FirebaseAuth.instance.currentUser.uid es una referencia al ID único generado automáticamente que Authentication proporciona a todos los usuarios que accedieron a sus cuentas.

  • En el archivo lib/app_state.dart, agrega el método addMessageToGuestBook. Debes conectar esta capacidad a la interfaz de usuario en el siguiente paso.

lib/app_state.dart

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

import 'firebase_options.dart';

class ApplicationState extends ChangeNotifier {

  // Current content of ApplicationState elided ...

  // Add from here...
  Future<DocumentReference> addMessageToGuestBook(String message) {
    if (!_loggedIn) {
      throw Exception('Must be logged in');
    }

    return FirebaseFirestore.instance
        .collection('guestbook')
        .add(<String, dynamic>{
      'text': message,
      'timestamp': DateTime.now().millisecondsSinceEpoch,
      'name': FirebaseAuth.instance.currentUser!.displayName,
      'userId': FirebaseAuth.instance.currentUser!.uid,
    });
  }
  // ...to here.
}

Cómo conectar la IU y la base de datos

Tienes una IU en la que el usuario puede ingresar el texto que desea agregar al libro de invitados y tienes el código para agregar la entrada a Firestore. Ahora, solo debes conectar los dos.

  • En el archivo lib/home_page.dart, realiza el siguiente cambio en el widget HomePage:

lib/home_page.dart

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

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

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Firebase Meetup'),
      ),
      body: ListView(
        children: <Widget>[
          Image.asset('assets/codelab.png'),
          const SizedBox(height: 8),
          const IconAndDetail(Icons.calendar_today, 'October 30'),
          const IconAndDetail(Icons.location_city, 'San Francisco'),
          Consumer<ApplicationState>(
            builder: (context, appState, _) => AuthFunc(
                loggedIn: appState.loggedIn,
                signOut: () {
                  FirebaseAuth.instance.signOut();
                }),
          ),
          const Divider(
            height: 8,
            thickness: 1,
            indent: 8,
            endIndent: 8,
            color: Colors.grey,
          ),
          const Header("What we'll be doing"),
          const Paragraph(
            'Join us for a day full of Firebase Workshops and Pizza!',
          ),
          // Modify from here...
          Consumer<ApplicationState>(
            builder: (context, appState, _) => Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                if (appState.loggedIn) ...[
                  const Header('Discussion'),
                  GuestBook(
                    addMessage: (message) =>
                        appState.addMessageToGuestBook(message),
                  ),
                ],
              ],
            ),
          ),
          // ...to here.
        ],
      ),
    );
  }
}

Reemplazaste las dos líneas que agregaste al comienzo de este paso por la implementación completa. Vuelve a usar Consumer<ApplicationState> a fin de que el estado de la app esté disponible para la parte del árbol que renderizas. Esto te permite reaccionar ante alguien que ingresa un mensaje en la IU y publicarlo en la base de datos. En la siguiente sección, probarás si los mensajes agregados se publican en la base de datos.

Pruebe enviar mensajes

  1. Si es necesario, accede a la app.
  2. Ingresa un mensaje, como Hey there!, y haz clic en ENVIAR.

Esta acción escribe el mensaje en tu base de datos de Firestore. Sin embargo, no verás el mensaje en tu app de Flutter real, ya que deberás implementar la recuperación de los datos, algo que harás en el siguiente paso. Sin embargo, en el panel Database de Firebase console, puedes ver el mensaje que agregaste en la colección guestbook. Si envías más mensajes, se agregan más documentos a tu colección de guestbook. Por ejemplo, consulta el siguiente fragmento de código:

713870af0b3b63c.png

7. Lea los mensajes

Es bueno que los invitados puedan escribir mensajes en la base de datos, pero que aún no puedan verlos en la app. Es hora de corregirlo.

Sincronizar mensajes

Para mostrar los mensajes, debes agregar objetos de escucha que se activen cuando los datos cambien y, luego, crear un elemento de la IU que muestre los mensajes nuevos. Agregarás código al estado de la app que escucha los mensajes recién agregados desde la app.

  1. Crea un archivo nuevo guest_book_message.dart y agrega la siguiente clase para exponer una vista estructurada de los datos que almacenas en Firestore.

lib/guest_book_message.dart

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

  final String name;
  final String message;
}
  1. En el archivo lib/app_state.dart, agrega las siguientes importaciones:

lib/app_state.dart

import 'dart:async';                                     // new

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

import 'firebase_options.dart';
import 'guest_book_message.dart';                        // new
  1. En la sección de ApplicationState, en la que defines el estado y los métodos get, agrega las siguientes líneas:

lib/app_state.dart

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

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

lib/app_state.dart

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

    FirebaseUIAuth.configureProviders([
      EmailAuthProvider(),
    ]);
    
    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loggedIn = true;
        _guestBookSubscription = FirebaseFirestore.instance
            .collection('guestbook')
            .orderBy('timestamp', descending: true)
            .snapshots()
            .listen((snapshot) {
          _guestBookMessages = [];
          for (final document in snapshot.docs) {
            _guestBookMessages.add(
              GuestBookMessage(
                name: document.data()['name'] as String,
                message: document.data()['text'] as String,
              ),
            );
          }
          notifyListeners();
        });
      } else {
        _loggedIn = false;
        _guestBookMessages = [];
        _guestBookSubscription?.cancel();
      }
      notifyListeners();
    });
  }

Esta sección es importante porque es donde construyes una consulta sobre la colección guestbook y te encargas de la suscripción y la anulación de suscripciones a esta colección. Escucharás la transmisión, en la que reconstruirás una caché local de los mensajes de la colección guestbook y, además, almacenarás una referencia a esta suscripción para que puedas anular la suscripción más adelante. Suceden muchas cosas, por lo que deberías explorarla en un depurador para revisar lo que sucede y obtener un modelo mental más claro. Si necesitas más información, consulta Obtén actualizaciones en tiempo real con Firestore.

  1. En el archivo lib/guest_book.dart, agrega la siguiente importación:
import 'guest_book_message.dart';
  1. En el widget GuestBook, agrega una lista de mensajes como parte de la configuración para conectar este estado cambiante a la interfaz de usuario:

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

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

Unes el contenido anterior del método build() con un widget Column y, luego, agregas una colección al final de los elementos secundarios de Column a fin de generar una Paragraph nueva para cada mensaje de la lista de mensajes.

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

lib/home_page.dart

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

Cómo probar la sincronización de mensajes

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

Prueba la sincronización de mensajes:

  1. En la app, busca los mensajes que creaste antes en la base de datos.
  2. Escribir mensajes nuevos Aparecen al instante.
  3. Abre tu espacio de trabajo en varias ventanas o pestañas. Los mensajes se sincronizan en tiempo real en todas las ventanas y pestañas.
  4. Opcional: En el menú Base de datos de Firebase console, borra, modifica o agrega mensajes nuevos de forma manual. Todos los cambios aparecen en la IU.

¡Felicitaciones! Lees documentos de Firestore en tu app.

Vista previa de la app

La pantalla principal de la app en Android con integración de chat

La pantalla principal de la app en iOS con integración de chat

La pantalla principal de la app en la Web con integración de chat

La pantalla principal de la app en macOS con integración de chat

8. Configura reglas de seguridad básicas

Inicialmente, configuraste Firestore para usar el modo de prueba, lo que significa que tu base de datos está abierta para operaciones de lectura y escritura. Sin embargo, solo debes usar el modo de prueba durante las primeras etapas del desarrollo. Como práctica recomendada, debes configurar reglas de seguridad para la base de datos mientras desarrollas la app. La seguridad es fundamental para la estructura y el comportamiento de la app.

Las reglas de seguridad de Firebase te permiten controlar el acceso a los documentos y colecciones de tu base de datos. La sintaxis de reglas flexibles te permite crear reglas que coincidan con cualquier acción, desde todas las operaciones de escritura en la base de datos hasta las operaciones en un documento específico.

Configura reglas de seguridad básicas:

  1. En el menú Desarrollar de Firebase console, haz clic en Base de datos > Reglas. Deberías ver las siguientes reglas de seguridad predeterminadas y una advertencia sobre las reglas que son públicas:

7767a2d2e64e7275.png

  1. Identifica las colecciones en las que la app escribe datos:

En match /databases/{database}/documents, identifica la colección que deseas 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 usaste el UID de autenticación como campo en cada documento del libro de visitas, puedes 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. Agrega las reglas de lectura y escritura a tu 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 que accedan a sus cuentas podrán leer los mensajes del libro de invitados, pero solo el autor de un mensaje puede editarlo.

  1. Agrega la validación de datos para asegurarte de 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 adicional: Practica lo que aprendiste

Graba el estado de confirmación de asistencia de un asistente

En este momento, tu app solo permite que las personas chateen cuando están interesadas en el evento. Además, la única manera de saber si alguien viene es cuando lo dice en el chat.

En este paso, te organizas e informas a las personas cuántas personas vienen. Agregarás un par de capacidades al estado de la app. La primera es la posibilidad de que un usuario que haya accedido a su cuenta decida si asistirá o no. El segundo es un contador de cuántas personas asistirán.

  1. En el archivo lib/app_state.dart, agrega las siguientes líneas a la sección de descriptores de acceso de ApplicationState para que el código de la IU pueda interactuar con este estado:

lib/app_state.dart

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

Attending _attending = Attending.unknown;
StreamSubscription<DocumentSnapshot>? _attendingSubscription;
Attending get attending => _attending;
set attending(Attending attending) {
  final userDoc = FirebaseFirestore.instance
      .collection('attendees')
      .doc(FirebaseAuth.instance.currentUser!.uid);
  if (attending == Attending.yes) {
    userDoc.set(<String, dynamic>{'attending': true});
  } else {
    userDoc.set(<String, dynamic>{'attending': false});
  }
}
  1. Actualiza el método init() de ApplicationState de la siguiente manera:

lib/app_state.dart

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

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

    // Add from here...
    FirebaseFirestore.instance
        .collection('attendees')
        .where('attending', isEqualTo: true)
        .snapshots()
        .listen((snapshot) {
      _attendees = snapshot.docs.length;
      notifyListeners();
    });
    // ...to here.

    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loggedIn = true;
        _emailVerified = user.emailVerified;
        _guestBookSubscription = FirebaseFirestore.instance
            .collection('guestbook')
            .orderBy('timestamp', descending: true)
            .snapshots()
            .listen((snapshot) {
          _guestBookMessages = [];
          for (final document in snapshot.docs) {
            _guestBookMessages.add(
              GuestBookMessage(
                name: document.data()['name'] as String,
                message: document.data()['text'] as String,
              ),
            );
          }
          notifyListeners();
        });
        // Add from here...
        _attendingSubscription = FirebaseFirestore.instance
            .collection('attendees')
            .doc(user.uid)
            .snapshots()
            .listen((snapshot) {
          if (snapshot.data() != null) {
            if (snapshot.data()!['attending'] as bool) {
              _attending = Attending.yes;
            } else {
              _attending = Attending.no;
            }
          } else {
            _attending = Attending.unknown;
          }
          notifyListeners();
        });
        // ...to here.
      } else {
        _loggedIn = false;
        _emailVerified = false;
        _guestBookMessages = [];
        _guestBookSubscription?.cancel();
        _attendingSubscription?.cancel(); // new
      }
      notifyListeners();
    });
  }

Este código agrega una consulta de suscripción permanente para determinar la cantidad de asistentes y una segunda consulta que solo está activa cuando el usuario accede a su cuenta para determinar si asistirá.

  1. Agrega la siguiente enumeración en la parte superior del archivo lib/app_state.dart.

lib/app_state.dart

enum Attending { yes, no, unknown }
  1. Crea un archivo nuevo yes_no_selection.dart y define un widget nuevo que actúe como botones de selección:

lib/yes_no_selection.dart

import 'package:flutter/material.dart';

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

class YesNoSelection extends StatelessWidget {
  const YesNoSelection(
      {super.key, required this.state, required this.onSelection});
  final Attending state;
  final void Function(Attending selection) onSelection;

  @override
  Widget build(BuildContext context) {
    switch (state) {
      case Attending.yes:
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            children: [
              FilledButton(
                onPressed: () => onSelection(Attending.yes),
                child: const Text('YES'),
              ),
              const SizedBox(width: 8),
              TextButton(
                onPressed: () => onSelection(Attending.no),
                child: const Text('NO'),
              ),
            ],
          ),
        );
      case Attending.no:
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            children: [
              TextButton(
                onPressed: () => onSelection(Attending.yes),
                child: const Text('YES'),
              ),
              const SizedBox(width: 8),
              FilledButton(
                onPressed: () => onSelection(Attending.no),
                child: const Text('NO'),
              ),
            ],
          ),
        );
      default:
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            children: [
              StyledButton(
                onPressed: () => onSelection(Attending.yes),
                child: const Text('YES'),
              ),
              const SizedBox(width: 8),
              StyledButton(
                onPressed: () => onSelection(Attending.no),
                child: const Text('NO'),
              ),
            ],
          ),
        );
    }
  }
}

Comienza en un estado indeterminado y no selecciona ni No. Una vez que el usuario seleccione si asistirá, mostrarás esa opción destacada con un botón relleno y la otra opción se desplazará con una renderización plana.

  1. Actualiza el método build() de HomePage para aprovechar YesNoSelection, permitir que un usuario que haya accedido nomine si asistirá y muestra la cantidad de asistentes al evento:

lib/home_page.dart

Consumer<ApplicationState>(
  builder: (context, appState, _) => Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      // Add from here...
      switch (appState.attendees) {
        1 => const Paragraph('1 person going'),
        >= 2 => Paragraph('${appState.attendees} people going'),
        _ => const Paragraph('No one going'),
      },
      // ...to here.
      if (appState.loggedIn) ...[
        // Add from here...
        YesNoSelection(
          state: appState.attending,
          onSelection: (attending) => appState.attending = attending,
        ),
        // ...to here.
        const Header('Discussion'),
        GuestBook(
          addMessage: (message) =>
              appState.addMessageToGuestBook(message),
          messages: appState.guestBookMessages,
        ),
      ],
    ],
  ),
),

Agrega reglas

Ya configuraste algunas reglas, por lo que se rechazarán los datos que agregues con los botones. Debes actualizar las reglas para permitir que se agreguen a la colección attendees.

  1. En la colección attendees, toma el UID de autenticación que usaste como nombre del documento y verifica que el uid del remitente 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;
    }
  }
}

De esta manera, todos pueden leer la lista de asistentes porque allí no hay datos privados, pero solo el creador puede actualizarlos.

  1. Agrega la validación de datos para asegurarte de 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 app, haz clic en botones para ver los resultados en el panel de Firestore en Firebase console.

Vista previa de la app

La pantalla principal de la app en Android

La pantalla principal de la app en iOS

La pantalla principal de la app en la Web

La pantalla principal de la app en macOS

10. ¡Felicitaciones!

Usaste Firebase para compilar una app web interactiva en tiempo real.

Más información