Conoce Firebase para Flutter

1. Antes de comenzar

En este codelab, 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 confirmación de asistencia para eventos en Android, iOS, la Web y macOS con Flutter.
  • Cómo autenticar usuarios con Firebase Authentication y sincronizar datos con Firestore.

La pantalla de inicio de la aplicación en Android

La pantalla de inicio de la aplicación en iOS

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 tu elección, como Google Chrome.
  • Un IDE o editor de texto de tu 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 tu proyecto de 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 de una colección de codelabs. El código para 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 su proyecto al final de cada paso. Por ejemplo, estás en el segundo paso.

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

Si desea avanzar 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 directorio flutter-codelabs/firebase-get-to-know-flutter/step_02 en su IDE preferido. Este directorio contiene el código inicial para el codelab, que consiste en una aplicación de reunión Flutter que aún no funciona.

Localice 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/home_page.dart : este archivo contiene el widget de la página de inicio.
    • 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 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 utilizan en la aplicación de inicio, pero los agregará pronto.

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

Revise el archivo lib/main.dart

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

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

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

La pantalla de inicio de la aplicación en Android

La pantalla de inicio de la aplicación en iOS

La pantalla de inicio de la aplicación en la web.

La pantalla de inicio de la aplicación en macOS

3. Crea y configura un proyecto de Firebase

La visualización de 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, debes conectar Firebase a tu aplicación. Para comenzar con Firebase, debe crear y configurar un proyecto de Firebase.

Crear un proyecto de Firebase

  1. Inicia sesión en Firebase .
  2. En la consola, haga clic en Agregar proyecto o Crear un proyecto .
  3. En el campo Nombre del proyecto , ingrese Firebase-Flutter-Codelab y luego haga 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 lo 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 debes habilitarlos en Firebase console.

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, expande el menú Generar .
  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 utiliza Firestore para guardar mensajes de chat y recibir nuevos mensajes de chat.

Habilitar Firestore:

  • En el menú Crear , haga clic en Base de datos de 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. Puedes usar el 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. Agrega las dependencias FlutterFire a tu 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 tu 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

Debes agregar las bibliotecas FlutterFire para los dos productos Firebase que usas 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 necesarios, pero también necesita configurar los proyectos de iOS, Android, macOS y Web runner para usar Firebase de manera adecuada. También utiliza el paquete 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 de Firebase y de las aplicaciones de proyecto seleccionadas para generar toda la configuración para una plataforma específica.

En la raíz de tu aplicación, ejecuta el comando 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 de las cuales extraer la configuración. De forma 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 Compatibilidad de escritorio con Flutter .

5. Agregue la funcionalidad RSVP

Ahora que agregaste Firebase a la aplicación, puedes crear un botón de confirmación de asistencia que registre a las personas con autenticación . Para Android nativo, iOS nativo y Web, existen paquetes FirebaseUI Auth prediseñados, pero es necesario 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 empresarial para integrar la autenticación con la aplicación.

Agregue lógica empresarial con el paquete Provider

Utilice el paquete 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. Cree un nuevo archivo 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 declaraciones import presentan Firebase Core y Auth, incorporan el paquete 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 aplicación ApplicationState tiene una responsabilidad principal para este paso, que es alertar al árbol de widgets de que hubo una actualización a un estado autenticado.

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, utiliza 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. Modifique 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. Conecte el estado de la aplicación con la inicialización de la aplicación y luego agregue 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 aplicación con el widget ChangeNotifierProvider . Se utiliza esta clase provider específica porque el objeto de estado de la aplicación extiende la clase ChangeNotifier , lo que le permite al paquete provider saber 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, creando una configuración GoRouter :

lib/main.dart

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

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

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

Cada pantalla tiene un tipo diferente de acción asociada según el nuevo estado del flujo de autenticación. Después de la mayoría de los cambios de estado en la autenticación, puede redirigir 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/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!',
          ),
        ],
      ),
    );
  }
}

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

Pruebe 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 solicitará que ingrese una contraseña. De lo contrario, el sistema le solicitará 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 de.
  2. Ingrese contraseñas incorrectas para verificar el flujo de manejo de errores.
  3. Ingrese 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. Escribe mensajes en Firestore

Es genial saber que los usuarios vendrán, 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 o a quién esperan conocer.

Para almacenar los mensajes de chat que los usuarios escriben en la aplicación, se 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 guestbook , 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 enviar, y luego agrega el código que conecta estos elementos con la base de datos.

  1. Cree un nuevo archivo llamado guest_book.dart , agregue un widget con estado GuestBook para construir los elementos de la interfaz de usuario de un campo de mensaje y un botón de enviar:

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

Hay un par de puntos de interés aquí. Primero, crea una instancia de un formulario para poder validar que el mensaje realmente contiene contenido y mostrarle al usuario un mensaje de error si no lo hay. 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é es necesario esto, consulte Comprender las restricciones .

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

  1. Edite el cuerpo de HomePage para agregar las dos líneas siguientes 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. Actualizará este código en breve para que sea funcional.

Vista previa de la aplicación

La pantalla de inicio de la aplicación en Android con integración de chat

La pantalla de inicio de la aplicación en iOS con integración de chat

La pantalla de inicio de la aplicación en la web con integración de chat.

La pantalla de inicio de la aplicación en macOS con integración de chat

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

Tenga en cuenta que FirebaseAuth.instance.currentUser.uid es una referencia al ID único generado automáticamente que la autenticación proporciona a todos los usuarios que han iniciado sesión.

  • En el archivo lib/app_state.dart , agregue el método addMessageToGuestBook . Esta capacidad se conecta con 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.
}

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 visitas y tiene el código para agregar la entrada a Firestore. Ahora todo lo que necesitas hacer es conectar los dos.

  • En el archivo lib/home_page.dart , realice el siguiente cambio en el widget de la 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.
        ],
      ),
    );
  }
}

Reemplazó las dos líneas que agregó al comienzo de este paso con la implementación completa. Nuevamente usa Consumer<ApplicationState> para que el estado de la aplicación esté disponible para la parte del árbol que representa. 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, probará si los mensajes agregados se publican en la base de datos.

Prueba de envío de mensajes

  1. Si es necesario, inicie sesión en la aplicación.
  2. Ingrese 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 ves el mensaje en tu aplicación Flutter real porque aún necesitas implementar la recuperación de datos, lo cual haces en el siguiente paso. Sin embargo, en el panel de base de datos de Firebase console, puedes ver el mensaje agregado en la colección guestbook . Si envía más mensajes, agrega más documentos a su colección guestbook . Por ejemplo, consulte el siguiente fragmento de código:

713870af0b3b63c.png

7. Leer mensajes

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

Sincronizar mensajes

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

  1. Cree un nuevo archivo guest_book_message.dart , agregue la siguiente clase para exponer una vista estructurada de los datos que almacena 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 , agregue 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 donde define el estado y los captadores, agregue 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 , 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 sesión:

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 se construye una consulta sobre la colección guestbook y se gestiona la suscripción y cancelación de la suscripción a esta colección. Escuchas la transmisión, donde reconstruyes un caché local de los mensajes en la colección guestbook y también almacenas una referencia a esta suscripción para que puedas cancelar tu suscripción más tarde. Están sucediendo muchas cosas aquí, por lo que debes explorarlas en un depurador para inspeccionar qué sucede y obtener un modelo mental más claro. Para obtener más información, consulte Obtenga actualizaciones en tiempo real con Firestore .

  1. En el archivo lib/guest_book.dart , agregue la siguiente importación:
import 'guest_book_message.dart';
  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/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 , modifique 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.
    );
  }
}

Envuelve el contenido anterior del método build() con un widget Column y luego agrega una colección al final de los elementos secundarios de la 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 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
        ),
      ],
    ],
  ),
),

Sincronización de mensajes de prueba

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

Sincronización de mensajes de prueba:

  1. En la aplicación, busque los mensajes que creó anteriormente en la base de datos.
  2. Escribe nuevos mensajes. Aparecen al instante.
  3. Abra su espacio de trabajo en varias 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 Firebase console, elimina, modifica o agrega mensajes nuevos 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

La pantalla de inicio de la aplicación en Android con integración de chat

La pantalla de inicio de la aplicación en iOS con integración de chat

La pantalla de inicio de la aplicación en la web con integración de chat.

La pantalla de inicio de la aplicación en macOS con integración de chat

8. Configure reglas de seguridad básicas

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, sólo debes utilizar 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 te permiten controlar el acceso a documentos y colecciones en tu base de datos. La sintaxis de reglas flexible 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 de seguridad básicas:

  1. En el menú Desarrollar de Firebase console, haz 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 utilizó el UID de autenticación como campo en cada documento del libro de visitas, puede obtener el UID de autenticación y verificar que cualquiera 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 que hayan iniciado sesión pueden leer los mensajes en el libro de visitas, pero solo el autor de un mensaje puede editarlo.

  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 adicional: practica lo que has aprendido

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

En este momento, tu aplicación solo permite a las personas chatear cuando están interesadas en el evento. Además, la única forma de saber si viene alguien es cuando lo dice en el chat.

En este paso, usted se organiza y le informa a la gente cuántas personas asistirán. 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 asisten.

  1. En el archivo lib/app_state.dart , agregue las siguientes líneas a la sección de accesos de ApplicationState para que el código de la interfaz de usuario 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. Actualice 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 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 asiste.

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

lib/app_state.dart

enum Attending { yes, no, unknown }
  1. Cree un nuevo archivo yes_no_selection.dart , defina un nuevo widget que actúe como botones de opció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 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 el número 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,
        ),
      ],
    ],
  ),
),

Agregar reglas

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

  1. En la colección attendees , tome el UID de autenticación que utilizó como nombre del documento y verifique que el uid del remitente sea el mismo que el 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 Firestore en Firebase console.

Vista previa de la aplicación

La pantalla de inicio de la aplicación en Android

La pantalla de inicio de la aplicación en iOS

La pantalla de inicio de la aplicación en la web.

La pantalla de inicio de la aplicación en macOS

10. ¡Felicitaciones!

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

Aprende más