App multiplataforma con Flutter y Firestore

Objetivos

En este codelab, compilarás una app multiplataforma para recomendar restaurantes con la tecnología de Flutter y Cloud Firestore.

La app terminada se puede ejecutar en Android, iOS y la Web, a partir de una única base de código de Dart.

5e7215e72fa571b.png

Qué aprenderás

  • Cómo leer y escribir datos en Cloud Firestore desde una app de Flutter
  • Cómo detectar cambios en datos de Cloud Firestore en tiempo real
  • Cómo usar Firebase Authentication y reglas de seguridad para proteger datos de Cloud Firestore
  • Cómo escribir consultas y transacciones complejas de Cloud Firestore

¿Qué te gustaría aprender en este codelab?

Soy nuevo en el tema y me gustaría ver una buena descripción general. Tengo algunos conocimientos sobre este tema, pero me gustaría repasarlo. Estoy buscando código de ejemplo para usar en mi proyecto. Estoy buscando una explicación sobre un tema específico.

Requisitos

Si no estás familiarizado con Flutter o Firestore, primero completa el codelab Firebase para Flutter:

Para completar este codelab, necesitas lo siguiente:

  • Un IDE o editor de texto que prefieras, como Android Studio o VS Code, que estén configurados con los complementos de Dart y Flutter
  • El navegador Google Chrome y algunos conocimientos sobre las herramientas para desarrolladores de Chrome
  • Una versión reciente de Flutter (canal beta o posterior) con compatibilidad web habilitada (puedes configurar la compatibilidad web durante este codelab, pero, para obtener más información, consulta la página compatibilidad web para Flutter)
  • La herramienta npm, con el fin de instalar las herramientas oficiales de línea de comandos firebase para la parte final de este codelab (Cómo implementar índices, Cómo proteger tus datos y Cómo implementar en Firebase Hosting)
  • Un dispositivo Android o emulador conectado si deseas compilar una app para Android (opcional)
  • Una Mac con una versión relativamente reciente de Xcode si deseas compilar una app para iOS (opcional)

Cómo crear un proyecto de Firebase

  1. En Firebase console, haz clic en Agregar proyecto y asígnale el nombre FriendlyEats al proyecto. Recuerda el ID del proyecto para tu proyecto de Firebase (o haz clic en el ícono Editar a fin de establecer el ID del proyecto que prefieras).
  2. Haz clic en Crear proyecto.

La aplicación que compilas usa varios servicios de Firebase disponibles en la Web:

  • Firebase Authentication para identificar a tus usuarios con mayor facilidad
  • Cloud Firestore para guardar datos estructurados en la nube y recibir notificaciones instantáneas cuando se actualizan los datos
  • Firebase Hosting para alojar y entregar elementos estáticos

A continuación, te explicaremos cómo configurar y habilitar los servicios con Firebase console.

Cómo habilitar la autenticación anónima:

Si bien la autenticación no es el tema central de este codelab, es importante tener algún tipo de autenticación en tu app. Usarás el acceso anónimo, lo que significa que al usuario se le brinda acceso, de forma silenciosa, sin que se le solicite.

Para habilitar el acceso anónimo, haz lo siguiente:

  1. En Firebase console, busca la sección Desarrollar en la barra de navegación izquierda.
  2. Haz clic en Autenticación y en la pestaña Método de acceso (o ve directamente a Firebase console).
  3. Habilita el proveedor de acceso Anónimo y haz clic en Guardar.

fee6c3ebdf904459.png

Habilitar el acceso anónimo permite que la aplicación les brinde, de manera silenciosa, acceso a tus usuarios cuando ingresan a la aplicación web. Para obtener más información, consulta la documentación sobre autenticación anónima.

Cómo habilitar Cloud Firestore

La app usa Cloud Firestore para guardar y recibir información sobre los restaurantes y las calificaciones.

Para habilitar Cloud Firestore, haz lo siguiente:

  1. En la sección Desarrollar de Firebase console, haz clic en Base de datos.
  2. Haz clic en Crear base de datos en el panel de Cloud Firestore.

57e83568e05c7710.png

  1. Selecciona la opción Comenzar en modo de prueba y haz clic en Habilitar después de leer 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. Más adelante en este codelab, mejorarás la seguridad de tu base de datos.

daef1061fc25acc7.png

Clona el repositorio de GitHub desde la línea de comandos:

git clone https://github.com/FirebaseExtended/codelab-friendlyeats-flutter.git friendlyeats-flutter

El código de muestra se debe clonar en el directorio 📁friendlyeats-flutter. A partir de este momento, asegúrate de ejecutar los comandos desde este directorio:

cd friendlyeats-flutter

Cómo importar la app de inicio

Abre o importa el directorio 📁friendlyeats-flutter al IDE que prefieras. Este directorio contiene el código de inicio para el codelab que consiste en una app para recomendar restaurantes que todavía no es funcional.

Durante este codelab, lograrás que la app sea funcional, por lo que podrás editar el código en ese directorio más adelante.

Cómo ubicar los archivos con los que trabajarás

Si bien el punto de entrada habitual para una app de Flutter es su archivo lib/main.dart, en este codelab, te enfocarás en el punto de vista de los datos.

Ubica los siguientes archivos en el proyecto:

  • lib/src/model/data.dart: Es el archivo principal que modificas durante este codelab. Contiene toda la lógica para leer y escribir datos desde Firestore.
  • web/index.html: Es el archivo que carga el navegador para iniciar tu aplicación. Modificas este archivo a fin de instalar e inicializar las bibliotecas de Firebase para la Web.

Firebase CLI

Firebase CLI, la interfaz de línea de comandos (CLI) de Firebase, te permite implementar tu aplicación web y tu configuración en Firebase directamente desde los archivos de tu proyecto.

  1. Para instalar la CLI, ejecuta el siguiente comando npm:
npm -g install firebase-tools
  1. Ejecuta el siguiente comando para verificar que la CLI se haya instalado de manera correcta:
firebase --version

Asegúrate de que la versión de Firebase CLI sea v7.4.0 o posterior.

  1. Ejecuta el siguiente comando para autorizar Firebase CLI:
firebase login

El repositorio que clonaste en el paso anterior ya tiene un archivo firebase.json con algunas opciones de configuración del proyecto ya listas (ubicación de otros archivos de configuración, implementación de hosting, etc.). Ahora, debes asociar tu copia de trabajo de la app, con tu proyecto de Firebase:

  1. Asegúrate de que la línea de comandos acceda al directorio local de la app.
  2. Ejecuta el siguiente comando para asociar tu app con el proyecto de Firebase:
firebase use --add
  1. Cuando se te solicite, selecciona el ID del proyecto y asígnale un alias a tu proyecto de Firebase.

Un alias es útil si tienes varios entornos (producción, etapa de pruebas, etc.). Sin embargo, en este codelab, solo debes usar el alias de default.

  1. Sigue las instrucciones que se brindan en la línea de comandos.

Cómo habilitar la compatibilidad web para Flutter

Para compilar tu app de Flutter a fin de que se ejecute en la Web, debes habilitar esta función (que, actualmente, se encuentra en versión beta). Para habilitar la compatibilidad web, introduce el siguiente código:

$ flutter channel beta
$ flutter upgrade
$ flutter config --enable-web

En el IDE, en los menús desplegables de los dispositivos, o en la línea de comandos con flutter devices, deberías ver enumerados Chrome y servidor web.

El dispositivo Chrome inicia Chrome automáticamente. El servidor web inicia un servidor que aloja la app para que puedas cargarla desde cualquier navegador.

Durante el desarrollo, usa el dispositivo Chrome de modo que puedas usar las herramientas para desarrolladores y usa el servidor web cuando desees realizar la prueba en otros navegadores.

Después de crear un proyecto de Firebase, puedes configurar una (o más) apps para usar ese proyecto de Firebase. Haz lo siguiente:

  • Registra en Firebase el ID específico de la plataforma de tu app.
  • Genera archivos de configuración para tu app.
  • Agrega la configuración en el lugar correcto dentro las carpetas de tu proyecto.

ac27fbbadff7a3b9.png

Si estás desarrollando tu app multiplataforma de Flutter, debes registrar cada plataforma en la que se ejecute tu app dentro del mismo proyecto de Firebase.

Este codelab se enfoca en la plataforma web, porque iOS y Android se tratan en el codelab Firebase para Flutter. Consulta ese codelab si deseas agregar compatibilidad con Android o iOS a tu app de FriendlyEats.

En tu app de Flutter, puedes encontrar un archivo web/index.html especial que se usa como punto de entrada para tu app cuando se ejecuta en la Web. Modifica ese punto de entrada con una opción específica de configuración para tu proyecto, de modo que tu aplicación web pueda conectarse al backend de Firebase.

Configuración para la Web

  1. En Firebase console, selecciona Descripción general del proyecto en la barra de navegación izquierda y haz clic en el botón Web en Comienza por agregar Firebase a tu app. Deberías ver el siguiente diálogo:

f76ed55f71f15953.png

  1. Asígnale un sobrenombre a tu app. Es el valor que se usa en la consola de Firestore para identificar la versión web de tu app
  2. Haz clic en Registrar app.
  3. Después de registrar tu app, el paso Agregar el SDK de Firebase te brinda algunos fragmentos de código que debes pegar en el archivo web/index.html de tu app de Flutter. Cuando se complete, debería verse de la siguiente manera:

web/index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>friendlyeats</title>

  <!-- The core Firebase JS SDK is always required and must be listed first -->
  <script src="https://www.gstatic.com/firebasejs/7.5.1/firebase-app.js"></script>

  <!-- TODO: Add SDKs for Firebase products that you want to use
      https://firebase.google.com/docs/web/setup#available-libraries -->

  <script>
    // Your web app's Firebase configuration
    var firebaseConfig = {
      apiKey: "YoUr_RaNdOm_API_kEy",
      authDomain: "your-project-name.firebaseapp.com",
      databaseURL: "https://your-project-name.firebaseio.com",
      projectId: "your-project-name",
      storageBucket: "your-project-name.appspot.com",
      messagingSenderId: "012345678901",
      appId: "1:109876543210:web:r4nd0mH3xH45h"
    };
    // Initialize Firebase
    firebase.initializeApp(firebaseConfig);
  </script>

</head>
<body>
  <script src="main.dart.js" type="application/javascript"></script>
</body>
</html>
  1. Observa que hay un elemento TODO en el código que recién pegaste. En este paso, corriges este problema. Como el codelab usa Firebase Auth y Firestore, agrega las etiquetas de secuencias de comandos para esos productos en este momento:

web/index.html

  ...
  <!-- TODO: Add SDKs for Firebase products that you want to use
      https://firebase.google.com/docs/web/setup#available-libraries -->

  <script src="https://www.gstatic.com/firebasejs/7.5.1/firebase-auth.js"></script>
  <script src="https://www.gstatic.com/firebasejs/7.5.1/firebase-firestore.js"></script>

  <script>
    // Your web app's Firebase configuration
    var firebaseConfig = {
      ...
  1. Guarda tu archivo web/index.html y haz clic en Ir a la consola en el diálogo Agregar Firebase a tu aplicación web.
  2. Tu app de Flutter está lista para conectarse a Firebase.

d43a19dbf1248134.png¡Encontraste algo especial!

La mayoría de los cambios de código que se necesitan para habilitar la compatibilidad con Firebase ya están registrados en el proyecto en el que estás trabajando. Sin embargo, a fin de agregar compatibilidad con plataformas móviles, debes seguir un proceso similar al que hiciste recién para la Web:

  • Registra la plataforma deseada en el proyecto de Firebase
  • Descarga el archivo de configuración específico de la plataforma y agrégalo al código.

En el directorio de nivel superior de tu app de Flutter, puedes encontrar subdirectorios que se llaman ios y android. Estos directorios contienen los archivos de configuración específicos de la plataforma para iOS y Android, respectivamente.

Configuración para iOS

  1. En Firebase console, selecciona Descripción general del proyecto en la barra de navegación izquierda y haz clic en el botón iOS en Comienza por agregar Firebase a tu app.

Deberías ver el siguiente diálogo:

c42139f18fb9a2ee.png

  1. El valor importante que se debes brindar es el ID del paquete de iOS. Para obtener el ID del paquete, sigue los siguientes tres pasos.
  1. En la herramienta de línea de comandos, ve al directorio de nivel superior de tu app de Flutter.
  2. Ejecuta el comando open ios/Runner.xcworkspace para abrir Xcode.
  1. En Xcode, haz clic en Runner de nivel superior en el panel izquierdo para que aparezca la pestaña General en el panel derecho, como se muestra en la captura de pantalla. Copia el valor de Bundle Identifier.

9733e26be329f329.png

  1. Vuelve al diálogo de Firebase, pega el valor de Bundle Identifier en el campo ID del paquete de iOS y haz clic en Registrar app.
  1. Todavía en Firebase, sigue las instrucciones para descargar el archivo de configuración GoogleService-Info.plist.
  2. Regresa a Xcode. Ten en cuenta que Runner tiene una subcarpeta que también se llama Runner (como se muestra en la imagen anterior).
  3. Arrastra el archivo GoogleService-Info.plist (que recién descargaste) a esa subcarpeta Runner.
  4. En el diálogo que aparece en Xcode, haz clic en Finish.
  5. Regresa a Firebase console. En el paso de configuración, haz clic en Siguiente, omite los pasos restantes y regresa a la página principal de Firebase console.

Ya terminaste de configurar tu app de Flutter para iOS.

Configuración para Android

  1. En Firebase console, selecciona Descripción general del proyecto en la barra de navegación izquierda y haz clic en el botón Android en Comienza por agregar Firebase a tu app.

Deberías ver el siguiente diálogo: 8254fc299e82f528.png

  1. El valor importante que se debes brindar es el nombre del paquete de Android. Para obtener el nombre del paquete, sigue los siguientes dos pasos:
  1. En el directorio de tu app de Flutter, abre el archivo android/app/src/main/AndroidManifest.xml.
  2. En el elemento manifest, busca el valor de la string del atributo package. Este valor es el nombre del paquete de Android (similar a com.yourcompany.yourproject). Cópialo.
  3. En el diálogo de Firebase, pega el nombre del paquete que copiaste en el campo Nombre del paquete de Android.
  4. Para este codelab, no necesitas el certificado de firma SHA-1 de depuración. Déjalo en blanco.
  5. Haz clic en Registrar app.
  6. Todavía en Firebase, sigue las instrucciones para descargar el archivo de configuración google-services.json.
  7. Ve al directorio de tu app de Flutter y mueve el archivo google-services.json (que recién descargaste) al directorio android/app.
  8. Regresa a Firebase console, omite los pasos restantes y regresa a la página principal de Firebase console.
  9. Ya registraste toda la configuración de Gradle. Si tu app todavía se está ejecutando, ciérrala y vuelve a compilarla para permitir que Gradle instale las dependencias.

Ya terminaste de configurar tu app de Flutter para Android.

Estás listo para comenzar a trabajar en tu app. Primero, ejecuta la app de manera local. Ahora, puedes ejecutar la app en cualquier plataforma que hayas configurado (y para las que tengas un dispositivo y un emulador disponibles).

Descubre qué dispositivos están disponibles con el siguiente comando:

flutter devices

Según los dispositivos disponibles, el resultado del comando anterior se ve de la siguiente manera:

3 connected devices:

Android SDK built for x86 • emulator-5554 • android-x86    • Android 7.1.1 (API 25) (emulator)
Chrome                    • chrome        • web-javascript • Google Chrome 79.0.3945.130
Web Server                • web-server    • web-javascript • Flutter Tools

Continuaremos este codelab con el dispositivo chrome.

  1. Ejecuta el siguiente comando de Flutter CLI:
flutter run -d chrome
  1. Flutter comienza con Compilando tu aplicación para la Web y se abre automáticamente una ventana de Chrome con la app en ejecución.

Ahora, deberías ver tu copia de FriendlyEats, conectada al proyecto de Firebase.

La app se conecta automáticamente a tu proyecto de Firebase y te permite acceder, de forma silenciosa, como usuario anónimo.

c45806a2ac9300d9.png

En esta sección, escribirás algunos datos en Cloud Firestore para propagar la IU de la app. Puedes hacerlo de forma manual con Firebase console, pero hazlo en la app para obtener una demostración de la escritura básica de Cloud Firestore.

Modelo de datos

Los datos de Firestore se dividen en colecciones, documentos, campos y subcolecciones. Cada restaurante se almacena como un documento en una colección de nivel superior que se llama restaurants.

92f8dc2c769d2d6c.png

Más adelante, almacenarás cada opinión en una subcolección que se llama ratings dentro de cada restaurant.

a00d9eb006ddd6c0.png

Cómo agregar restaurantes a Firestore

Un restaurante es el objeto principal del modelo en la app. A continuación, escribirás código que agregue un documento de restaurante a la colección restaurants.

  1. Abre lib/src/model/data.dart.
  2. Busca la función addRestaurant.
  3. Reemplaza toda la función por el siguiente código.

lib/src/model/data.dart

Future<void> addRestaurant(Restaurant restaurant) {
  final restaurants = FirebaseFirestore.instance.collection('restaurants');
  return restaurants.add({
    'avgRating': restaurant.avgRating,
    'category': restaurant.category,
    'city': restaurant.city,
    'name': restaurant.name,
    'numRatings': restaurant.numRatings,
    'photo': restaurant.photo,
    'price': restaurant.price,
  });
}

El código anterior agrega un documento nuevo a la colección restaurants.

Para ello, primero debes obtener una referencia a una colección de Cloud Firestore restaurants y, luego, adding los datos.

Los datos del documento provienen de un objeto Restaurant, que se debe convertir en un elemento Map para el complemento de Firestore.

Cómo agregar algunos restaurantes

  1. Vuelve a compilar y actualiza tu app de Flutter (Mayúsculas + R en la ventana de la terminal que ejecuta tu app).
  2. Haz clic en ADD SOME.

La app genera automáticamente un conjunto aleatorio de objetos restaurants y llama a la función addRestaurant. Sin embargo, no verás los datos en tu aplicación web porque todavía debes implementar la opción para recuperar los datos (en la próxima sección del codelab).

Si navegas hasta la pestaña Desarrollar > Base de datos > Cloud Firestore en Firebase console, deberías ver documentos nuevos en la colección restaurants.

f06898b9d6dd4881.png

¡Felicitaciones! Acabas de escribir datos en Cloud Firestore desde una aplicación web.

En la siguiente sección, aprenderás a recuperar datos desde Cloud Firestore y mostrarlos en tu app.

En esta sección, aprenderás cómo recuperar datos desde Cloud Firestore y mostrarlos en tu app. Los dos pasos clave consisten en crear una consulta y detectar a su elemento Stream de las instantáneas. Este objeto de escucha recibe una notificación de todos los datos existentes que coinciden con la consulta y recibe actualizaciones en tiempo real.

Primero, construye la consulta que entregue la lista predeterminada de restaurantes sin filtros.

  1. Regresa al archivo lib/src/model/data.dart.
  2. Busca la función loadAllRestaurants.
  3. Reemplaza toda la función por el siguiente código.

lib/src/model/data.dart

Stream<QuerySnapshot> loadAllRestaurants() {
  return FirebaseFirestore.instance
      .collection('restaurants')
      .orderBy('avgRating', descending: true)
      .limit(50)
      .snapshots();
}

El código anterior construye una consulta que recupera hasta 50 restaurantes de la colección de nivel superior con el nombre restaurants, ordenados por su calificación promedio (en este momento, todos en cero).

Ahora, debes transformar cada QuerySnapshot que se muestra desde el elemento Stream en los datos Restaurant que puedes renderizar.

Para extraer información Restaurant de un elemento QuerySnapshot de la colección restaurants, haz lo siguiente:

  1. Regresa al archivo lib/src/model/data.dart.
  2. Busca la función getRestaurantsFromQuery.
  3. Reemplaza toda la función por el siguiente código:

lib/src/model/data.dart

List<Restaurant> getRestaurantsFromQuery(QuerySnapshot snapshot) {
  return snapshot.docs.map((DocumentSnapshot doc) {
    return Restaurant.fromSnapshot(doc);
  }).toList();
}

Se llama al método getRestaurantsFromQuery cada vez que se encuentra un nuevo elemento QuerySnapshot del objeto Query que creaste antes. QuerySnapshots son el mecanismo que Firestore usa para notificarle a tu app los cambios que se realicen en el objeto Query, en tiempo real.

Este método solo convierte todos los objetos documents que contiene snapshot en elementos Restaurant que se pueden usar en otras partes de tu app de Flutter.

Ahora que implementaste ambos métodos, vuelve a compilar y a cargar tu app, y verifica que los restaurantes que viste antes en Firebase console se puedan observar en la app. Si completaste esta sección con éxito, tu app lee y escribe datos con Cloud Firestore.

A medida que cambie tu lista de restaurantes, este objeto de escucha se actualiza automáticamente. Prueba ir a Firebase console y borrar, de forma manual, un restaurante o cambiar su nombre. Observarás que los cambios aparecen en el sitio de inmediato.

edd9adbafa5bd539.png

Hasta ahora, aprendiste cómo usar onSnapshot para recuperar actualizaciones en tiempo real. Sin embargo, no siempre es lo que deseas hacer. A veces, tiene sentido solo recuperar los datos una vez.

Necesitas un método que cargue un restaurante específico desde su ID, para el momento en que los usuarios hagan clic en un restaurante específico en la app.

  1. Regresa al archivo lib/src/model/data.dart.
  2. Busca la función getRestaurant.
  3. Reemplaza toda la función por el siguiente código:

lib/src/model/data.dart

Future<Restaurant> getRestaurant(String restaurantId) {
  return FirebaseFirestore.instance
      .collection('restaurants')
      .doc(restaurantId)
      .get()
      .then((DocumentSnapshot doc) => Restaurant.fromSnapshot(doc));
}

El código usa get() para recuperar un objeto Future<DocumentSnapshot> que contiene la información del restaurante que solicitaste. Solo necesitas canalizar este objeto a través de then() a una función que convierta el parámetro DocumentSnapshot en un objeto Restaurant siempre que esté listo.

Después de implementar este método, puedes ver la página de detalles de cada restaurante.

  1. Para actualizar la app, presiona Mayúsculas + R en la terminal donde se ejecuta flutter.
  2. Haz clic en un restaurante de la lista y verás la página de detalles del restaurante:

f8ca540dda5540a9.png

A continuación, agregarás el código que se necesita para incluir calificaciones a restaurantes mediante transacciones.

En esta sección, puedes permitir que los usuarios les envíen opiniones a los restaurantes. Hasta ahora, todas tus escrituras son atómicas y relativamente sencillas. Si se produjo un error en alguna de las escrituras, es probable que solo debas solicitarle al usuario que vuelva a escribir, o tu app volverá a intentarlo automáticamente.

Tu app tendrá muchos usuarios que desean agregar una calificación para un restaurante, por lo que debes coordinar varias lecturas y escrituras. En primer lugar, se debe enviar la opinión, y luego se deben actualizar las calificaciones count y average rating del restaurante. Si un elemento falla, pero el otro no, se producirá un estado incoherente. Los datos de una parte de la base de datos no coinciden con los de la otra.

Afortunadamente, Cloud Firestore brinda una funcionalidad de transacción que te permite realizar varias lecturas y escrituras en una única operación atómica, lo que garantiza la coherencia de tus datos.

  1. Regresa al archivo lib/src/model/data.dart.
  2. Busca la función addReview.
  3. Reemplaza toda la función por el siguiente código:

lib/src/model/data.dart

Future<void> addReview({String restaurantId, Review review}) {
  final restaurant =
      FirebaseFirestore.instance.collection('restaurants').doc(restaurantId);
  final newReview = restaurant.collection('ratings').doc();

  return FirebaseFirestore.instance.runTransaction((Transaction transaction) {
    return transaction
        .get(restaurant)
        .then((DocumentSnapshot doc) => Restaurant.fromSnapshot(doc))
        .then((Restaurant fresh) {
      final newRatings = fresh.numRatings + 1;
      final newAverage =
          ((fresh.numRatings * fresh.avgRating) + review.rating) / newRatings;

      transaction.update(restaurant, {
        'numRatings': newRatings,
        'avgRating': newAverage,
      });

      transaction.set(newReview, {
        'rating': review.rating,
        'text': review.text,
        'userName': review.userName,
        'timestamp': review.timestamp ?? FieldValue.serverTimestamp(),
        'userId': review.userId,
      });
    });
  });
}

La función anterior activa una transacción que comienza recuperando una versión fresh del elemento Restaurant que representa restaurantId.

Luego, actualizas los valores numéricos de avgRating y numRatings en la referencia del documento restaurant.

Al mismo tiempo, agregas el nuevo elemento review a través de la referencia del documento newReview en la subcolección ratings del restaurante.

Para probar el código que recién agregaste, haz lo siguiente:

  1. Para actualizar la app, presiona Mayúsculas + R en la terminal donde se ejecuta flutter.
  2. Ve a la página de detalles de cualquier restaurante.
  3. Agrega algunas opiniones. Para ello, haz lo siguiente:
  • Haz clic en el botón ADD SOME en la lista vacía si todavía no se agregó ninguna.
  • Haz clic en el botón de acción flotante + y escribe tu propia opinión

Por el momento, la app muestra una lista de restaurantes, pero el usuario no tiene manera de filtrar según sus necesidades. En esta sección, usarás las consultas avanzadas de Cloud Firestore para habilitar filtros.

A continuación, se muestra un ejemplo de una consulta simple para recuperar todos los restaurantes Dim Sum:

Query filteredCollection = FirebaseFirestore.instance
        .collection('restaurants')
        .where('category', isEqualTo: 'Dim Sum');

Como su nombre lo indica, el método where() permite que la consulta solo descargue los miembros de la colección cuyos campos cumplan con las restricciones que configuraste. En este caso, solo descarga restaurantes en los que el elemento category sea igual a Dim Sum.

De manera similar, puedes ordenar los datos que se muestran:

Query filteredAndSortedCollection = FirebaseFirestore.instance
        .collection('restaurants')
        .where('category', isEqualTo: 'Dim Sum')
        .orderBy('price', descending: true);

El método orderBy() permite que la consulta ordene los restaurantes Dim Sum por su atributo price, de mayor a menor costo.

En la app, el usuario puede encadenar varios filtros para crear consultas específicas, como Pizza en San Francisco o Mariscos en Los Ángeles, ordenadas por popularidad.

Creas un método que compile una consulta que filtre los restaurantes según varios criterios que seleccionen los usuarios.

  1. Regresa al archivo lib/src/model/data.dart.
  2. Busca la función loadFilteredRestaurants.
  3. Reemplaza toda la función por el siguiente código:

lib/src/model/data.dart

Stream<QuerySnapshot> loadFilteredRestaurants(Filter filter) {
  Query collection = FirebaseFirestore.instance.collection('restaurants');
  if (filter.category != null) {
    collection = collection.where('category', isEqualTo: filter.category);
  }
  if (filter.city != null) {
    collection = collection.where('city', isEqualTo: filter.city);
  }
  if (filter.price != null) {
    collection = collection.where('price', isEqualTo: filter.price);
  }
  return collection
      .orderBy(filter.sort ?? 'avgRating', descending: true)
      .limit(50)
      .snapshots();
}

El código anterior agrega varios filtros where y una sola cláusula orderBy para compilar una consulta compuesta según las entradas del usuario. Ahora, la consulta solo muestra restaurantes que coincidan con los requisitos del usuario.

Para actualizar la app en el navegador, presiona Mayúsculas + R en la terminal donde se ejecuta flutter.

Ahora, prueba filtrar por precio, ciudad y categoría. Mientras realizas las pruebas, observarás errores en la Consola de JavaScript de tu navegador que se ven de la siguiente manera:

The query requires an index. You can create it here: https://console.firebase.google.com/project/.../database/firestore/indexes?create_index=...

Estos errores ocurren porque Cloud Firestore requiere índices para la mayoría de las consultas compuestas. Exigir la indexación en las consultas mantiene la velocidad de Cloud Firestore a gran escala.

Abrir el vínculo desde el mensaje de error abre automáticamente la IU para crear índices en Firebase console con los parámetros correctos ya completos.

En la siguiente sección, escribirás e implementarás los índices que se necesitan para esta aplicación, todos al mismo tiempo, desde Firebase CLI.

Si no deseas explorar todas las rutas en la app y seguir cada uno de los vínculos para crear índices, puedes implementar fácilmente muchos índices a la vez con Firebase CLI.

  1. En la raíz del proyecto de tu app, busca el archivo firestore.indexes.json.

Este archivo describe todos los índices que se necesitan para todas las combinaciones posibles de filtros.

firestore.indexes.json

{
 "indexes": [
   {
     "collectionGroup": "restaurants",
     "queryScope": "COLLECTION",
     "fields": [
       { "fieldPath": "city", "order": "ASCENDING" },
       { "fieldPath": "avgRating", "order": "DESCENDING" }
     ]
   },

   ...

 ]
}
  1. Implementa estos índices con el siguiente comando:
firebase deploy --only firestore:indexes

Después de unos minutos, se publicarán tus índices, y desaparecerán los mensajes de error. Si intentas usar los índices antes de que estén listos, es posible que veas errores similares a los siguientes:

The query requires an index. That index is currently building and cannot be used yet. See its status here:
https://console.firebase.google.com/project/.../database/firestore/indexes?create_index=...

Al principio de este codelab, configuraste las reglas de seguridad de tu app para abrir la base de datos por completo en cualquier lectura o escritura. En una aplicación real, configurarás reglas mucho más específicas para evitar el acceso o la modificación de datos no deseados.

.

  1. En la sección Desarrollar de Firebase console, haz clic en Base de datos.
  2. Haz clic en la pestaña Reglas en la sección Cloud Firestore (o accede directamente a Firebase console).
  3. Reemplaza los valores predeterminados por las siguientes reglas y haz clic en Publicar.

firestore.rules

service cloud.firestore {
  match /databases/{database}/documents {
    // Restaurants:
    //   - Authenticated user can read
    //   - Authenticated user can create/update (for demo)
    //   - Validate updates
    //   - Deletes are not allowed
    match /restaurants/{restaurantId} {
      allow read, create: if request.auth != null;
      allow update: if request.auth != null
                    && request.resource.data.name == resource.data.name
      allow delete: if false;

      // Ratings:
      //   - Authenticated user can read
      //   - Authenticated user can create if userId matches
      //   - Deletes and updates are not allowed
      match /ratings/{ratingId} {
        allow read: if request.auth != null;
        allow create: if request.auth != null
                      && request.resource.data.userId == request.auth.uid;
        allow update, delete: if false;
      }
    }
  }
}

Estas reglas restringen el acceso para garantizar que los clientes solo realicen cambios seguros, por ejemplo:

  • Las actualizaciones de un documento de restaurante solo pueden cambiar las calificaciones, pero no el nombre ni otros datos inmutables.
  • Solo se pueden crear calificaciones si el ID del usuario coincide con el usuario que accedió, lo que evita la falsificación de identidad.

En lugar de usar Firebase console, puedes usar Firebase CLI para implementar reglas en tu proyecto de Firebase. El archivo firestore.rules del directorio de trabajo ya contiene las reglas anteriores. Para implementar estas reglas desde tu sistema local de archivos (en lugar de usar Firebase console), ejecuta el siguiente comando:

firebase deploy --only firestore:rules

flutter build web

Hasta ahora, solo usaste versiones de "depuración" de tu app de Flutter. Esas compilaciones son un poco más lentas, ya que contienen información adicional para facilitar la depuración.

Antes de implementar la app, debes compilar una versión de producción (prod). Flutter te permite compilar una app de producción con la herramienta build:

flutter build web

De esta manera, se colocan todos los elementos compilados para producción en el directorio build/web del proyecto.

Tu app ya está lista para implementarse en Firebase.

firebase deploy

Tu proyecto se preconfiguró para compilar e implementar los elementos que genera Flutter (observa el archivo firebase.json en la raíz del proyecto).

Implementa una versión nueva de tu app con Firebase mediante los siguientes comandos:

firebase init hosting
firebase deploy --only hosting

El proceso anterior debería tardar solo unos segundos. Borra cualquier compilación anterior (flutter clean), vuelve a compilar tu app (flutter build web) e implementa los elementos recién compilados (el contenido de build/web) en Firebase Hosting.

El mensaje de éxito contiene una URL de Hosting en la que tu app publicada ya está disponible en Internet.

¡Felicitaciones!

En este codelab, aprendiste a conectar tu aplicación web de Flutter a Firebase con los complementos de Firebase Auth y Firestore, realizaste lecturas y escrituras básicas y avanzadas con Cloud Firestore, y también accediste a datos seguros con las reglas de seguridad.

Puedes encontrar la solución completa en la rama done del repositorio.

Para obtener más información sobre Dart y Flutter, visita sus sitios oficiales:

Para obtener más información sobre Cloud Firestore, consulta los siguientes recursos:

Este codelab es un buen punto de partida para explorar otras funciones de Firestore (y Firebase). Si deseas superar otro desafío, puedes probar lo siguiente:

  • Usa escrituras en lotes, en el método addRestaurantsBatch, para agregar todos los restaurantes y las opiniones en una misma solicitud, de modo que la IU de tu aplicación se actualice una sola vez.
  • Modifica el widget RestaurantAppBar para que actualice la calificación por estrellas de un restaurante en tiempo real, a medida que los usuarios agregan opiniones sobre este.
  • Habilita firebase_auth con google_sign_in para recuperar el nombre real del usuario que publica una opinión porque todos los usuarios son anónimos en este momento.