Codelab para Android de Cloud Firestore

1. Descripción general

Objetivos

En este codelab, compilarás una app de recomendación de restaurantes en Android respaldada por Cloud Firestore. Aprenderás a hacer lo siguiente:

  • Lee y escribe datos en Firestore desde una app para Android
  • Detecta cambios en datos de Firestore en tiempo real
  • Usa Firebase Authentication y reglas de seguridad para proteger los datos de Firestore
  • Escribe consultas complejas de Firestore

Requisitos previos

Antes de comenzar este codelab, asegúrate de haber hecho lo siguiente:

  • Android Studio Flamingo o una versión más reciente
  • Un emulador de Android con el nivel de API 19 o posterior
  • Node.js versión 16 o posterior
  • Java 17 o versiones posteriores

2. Crea un proyecto de Firebase

  1. Accede a Firebase console con tu Cuenta de Google.
  2. En Firebase console, haz clic en Agregar proyecto.
  3. Como se muestra en la siguiente captura de pantalla, ingresa un nombre para tu proyecto de Firebase (por ejemplo, "Friendly Eats") y haz clic en Continuar.

9d2f625aebcab6af.png

  1. Es posible que se te solicite que habilites Google Analytics. Para los fines de este codelab, tu selección no es importante.
  2. Después de un minuto aproximadamente, tu proyecto de Firebase estará listo. Haz clic en Continuar.

3. Configura el proyecto de muestra

Descarga el código

Ejecuta el siguiente comando para clonar el código de muestra de este codelab. Se creará una carpeta llamada friendlyeats-android en tu máquina:

$ git clone https://github.com/firebase/friendlyeats-android

Si no tienes Git en tu máquina, también puedes descargar el código directamente desde GitHub.

Agrega la configuración de Firebase

  1. En Firebase console, selecciona Descripción general del proyecto en el panel de navegación izquierdo. Haz clic en el botón Android para seleccionar la plataforma. Cuando se te solicite un nombre de paquete, usa com.google.firebase.example.fireeats.

730151ed16016421.png

  1. Haz clic en Register App, sigue las instrucciones para descargar el archivo google-services.json y muévelo a la carpeta app/ del código que acabas de descargar. Luego, haga clic en Siguiente.

Importa el proyecto

Abre Android Studio. Haz clic en File > New > Import Project y selecciona la carpeta friendlyeats-android.

4. Configura los emuladores de Firebase

En este codelab, usarás Firebase Emulator Suite para emular Cloud Firestore y otros servicios de Firebase de forma local. Esto proporciona un entorno de desarrollo local seguro, rápido y sin costo para compilar tu app.

Instala Firebase CLI

Primero, debes instalar Firebase CLI. Si usas macOS o Linux, puedes ejecutar el siguiente comando cURL:

curl -sL https://firebase.tools | bash

Si usas Windows, lee las instrucciones de instalación para obtener un objeto binario independiente o instalarlo a través de npm.

Una vez que hayas instalado la CLI, ejecutar firebase --version debería informar una versión de 9.0.0 o superior:

$ firebase --version
9.0.0

Acceder

Ejecuta firebase login para conectar la CLI a tu Cuenta de Google. Se abrirá una ventana nueva del navegador para completar el proceso de acceso. Asegúrate de elegir la misma cuenta que usaste cuando creaste el proyecto de Firebase.

Desde la carpeta friendlyeats-android, ejecuta firebase use --add para conectar el proyecto local con el proyecto de Firebase. Sigue las indicaciones para seleccionar el proyecto que creaste antes y, si se te solicita que elijas un alias, ingresa default.

5. Ejecuta la app

Ahora es momento de ejecutar por primera vez Firebase Emulator Suite y la app de FriendlyEats para Android.

Ejecuta los emuladores

En tu terminal, desde el directorio friendlyeats-android, ejecuta firebase emulators:start para iniciar los emuladores de Firebase. Deberías ver registros como los siguientes:

$ firebase emulators:start
i  emulators: Starting emulators: auth, firestore
i  firestore: Firestore Emulator logging to firestore-debug.log
i  ui: Emulator UI logging to ui-debug.log

┌─────────────────────────────────────────────────────────────┐
│ ✔  All emulators ready! It is now safe to connect your app. │
│ i  View Emulator UI at http://localhost:4000                │
└─────────────────────────────────────────────────────────────┘

┌────────────────┬────────────────┬─────────────────────────────────┐
│ Emulator       │ Host:Port      │ View in Emulator UI             │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Authentication │ localhost:9099 │ http://localhost:4000/auth      │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Firestore      │ localhost:8080 │ http://localhost:4000/firestore │
└────────────────┴────────────────┴─────────────────────────────────┘
  Emulator Hub running at localhost:4400
  Other reserved ports: 4500

Issues? Report them at https://github.com/firebase/firebase-tools/issues and attach the *-debug.log files.

Ahora tienes un entorno de desarrollo local completo en ejecución en tu máquina. Asegúrate de dejar este comando en ejecución durante el resto del codelab, ya que tu app para Android deberá conectarse a los emuladores.

Cómo conectar la app a los emuladores

Abre los archivos util/FirestoreInitializer.kt y util/AuthInitializer.kt en Android Studio. Estos archivos contienen la lógica necesaria para conectar los SDK de Firebase a los emuladores locales que se ejecutan en tu máquina cuando se inicia la aplicación.

En el método create() de la clase FirestoreInitializer, examina este fragmento de código:

    // Use emulators only in debug builds
    if (BuildConfig.DEBUG) {
        firestore.useEmulator(FIRESTORE_EMULATOR_HOST, FIRESTORE_EMULATOR_PORT)
    }

Usamos BuildConfig para asegurarnos de conectarnos solo a los emuladores cuando nuestra app se ejecute en modo debug. Cuando compilamos la app en modo release, esta condición será falsa.

Podemos ver que se usa el método useEmulator(host, port) para conectar el SDK de Firebase al emulador de Firestore local. En toda la app, usaremos FirebaseUtil.getFirestore() para acceder a esta instancia de FirebaseFirestore, de modo que podamos estar seguros de que siempre nos conectaremos al emulador de Firestore cuando se ejecute en el modo debug.

Ejecuta la app

Si agregaste el archivo google-services.json correctamente, ahora se debería compilar el proyecto. En Android Studio, haz clic en Build > Rebuild Project y asegúrate de que no haya errores restantes.

En Android Studio, ejecuta la app en tu emulador de Android. Al principio, verás una pantalla de "Acceso". Puedes usar cualquier correo electrónico y contraseña para acceder a la app. Este proceso de acceso se conecta al emulador de Firebase Authentication, por lo que no se transmiten credenciales reales.

Ahora, navega a http://localhost:4000 en tu navegador web para abrir la IU del emulador. Luego, haz clic en la pestaña Autenticación para ver la cuenta que acabas de crear:

Emulador de Firebase Auth

Una vez que hayas completado el proceso de acceso, deberías ver la pantalla principal de la app:

de06424023ffb4b9.png

Pronto agregaremos algunos datos para propagar la pantalla principal.

6. Escribe datos en Firestore

En esta sección, escribiremos algunos datos en Firestore para poder propagar la pantalla principal que está vacía.

El objeto principal del modelo de nuestra app es un restaurante (consulta model/Restaurant.kt). Los datos de Firestore se dividen en documentos, colecciones y subcolecciones. Almacenaremos cada restaurante como un documento en una colección de nivel superior llamada "restaurants". Para obtener más información sobre el modelo de datos de Firestore, consulta los documentos y las colecciones en la documentación.

A los efectos de la demostración, agregaremos funcionalidad a la app para crear diez restaurantes aleatorios cuando hagamos clic en el botón “Add Random Items” en el menú ampliado. Abre el archivo MainFragment.kt y reemplaza el contenido del método onAddItemsClicked() con lo siguiente:

    private fun onAddItemsClicked() {
        val restaurantsRef = firestore.collection("restaurants")
        for (i in 0..9) {
            // Create random restaurant / ratings
            val randomRestaurant = RestaurantUtil.getRandom(requireContext())

            // Add restaurant
            restaurantsRef.add(randomRestaurant)
        }
    }

Debes tener en cuenta algunos aspectos importantes sobre el código anterior:

  • Comenzamos por obtener una referencia a la colección "restaurants". Las colecciones se crean de manera implícita cuando se agregan documentos, por lo que no es necesario crear la colección antes de escribir datos.
  • Los documentos se pueden crear con clases de datos de Kotlin, que usamos para crear cada documento de Restaurant.
  • El método add() agrega un documento a una colección con un ID generado automáticamente, por lo que no tuvimos que especificar un ID único para cada restaurante.

Ahora vuelve a ejecutar la app y haz clic en el botón "Add Random Items" en el menú ampliado (en la esquina superior derecha) para invocar el código que acabas de escribir:

95691e9b71ba55e3.png

Ahora, navega a http://localhost:4000 en tu navegador web para abrir la IU del emulador. Luego, haz clic en la pestaña Firestore y deberías ver los datos que acabas de agregar:

Emulador de Firebase Auth

Estos datos son 100% locales en tu máquina. De hecho, tu proyecto real aún no contiene una base de datos de Firestore. Esto significa que es seguro modificar y borrar estos datos sin consecuencias.

Felicitaciones, acabas de escribir datos en Firestore. En el siguiente paso, aprenderemos a mostrar estos datos en la app.

7. Muestra datos de Firestore

En este paso, aprenderemos a recuperar datos de Firestore y mostrarlos en nuestra app. El primer paso para leer datos de Firestore es crear un Query. Abre el archivo MainFragment.kt y agrega el siguiente código al comienzo del método onViewCreated():

        // Firestore
        firestore = Firebase.firestore

        // Get the 50 highest rated restaurants
        query = firestore.collection("restaurants")
            .orderBy("avgRating", Query.Direction.DESCENDING)
            .limit(LIMIT.toLong())

Ahora queremos escuchar la consulta para que recibamos todos los documentos coincidentes y nos notifiquemos sobre las actualizaciones futuras en tiempo real. Como nuestro eventual objetivo es vincular estos datos a un RecyclerView, debemos crear una clase RecyclerView.Adapter para escuchar los datos.

Abre la clase FirestoreAdapter, que ya se implementó parcialmente. Primero, hagamos que el adaptador implemente EventListener y definamos la función onEvent para que pueda recibir actualizaciones de una consulta de Firestore:

abstract class FirestoreAdapter<VH : RecyclerView.ViewHolder>(private var query: Query?) :
        RecyclerView.Adapter<VH>(),
        EventListener<QuerySnapshot> { // Add this implements
    
    // ...

    // Add this method
    override fun onEvent(documentSnapshots: QuerySnapshot?, e: FirebaseFirestoreException?) {
        
        // Handle errors
        if (e != null) {
            Log.w(TAG, "onEvent:error", e)
            return
        }

        // Dispatch the event
        if (documentSnapshots != null) {
            for (change in documentSnapshots.documentChanges) {
                // snapshot of the changed document
                when (change.type) {
                    DocumentChange.Type.ADDED -> {
                        // TODO: handle document added
                    }
                    DocumentChange.Type.MODIFIED -> {
                        // TODO: handle document changed
                    }
                    DocumentChange.Type.REMOVED -> {
                        // TODO: handle document removed
                    }
                }
            }
        }

        onDataChanged()
    }
    
    // ...
}

Durante la carga inicial, el objeto de escucha recibirá un evento ADDED por cada documento nuevo. Como el conjunto de resultados de la consulta cambia con el tiempo, el objeto de escucha recibirá más eventos que contengan los cambios. Ahora, terminemos de implementar el objeto de escucha. Primero, agrega tres métodos nuevos: onDocumentAdded, onDocumentModified y onDocumentRemoved:

    private fun onDocumentAdded(change: DocumentChange) {
        snapshots.add(change.newIndex, change.document)
        notifyItemInserted(change.newIndex)
    }

    private fun onDocumentModified(change: DocumentChange) {
        if (change.oldIndex == change.newIndex) {
            // Item changed but remained in same position
            snapshots[change.oldIndex] = change.document
            notifyItemChanged(change.oldIndex)
        } else {
            // Item changed and changed position
            snapshots.removeAt(change.oldIndex)
            snapshots.add(change.newIndex, change.document)
            notifyItemMoved(change.oldIndex, change.newIndex)
        }
    }

    private fun onDocumentRemoved(change: DocumentChange) {
        snapshots.removeAt(change.oldIndex)
        notifyItemRemoved(change.oldIndex)
    }

Luego, llama a estos métodos nuevos desde onEvent:

    override fun onEvent(documentSnapshots: QuerySnapshot?, e: FirebaseFirestoreException?) {

        // Handle errors
        if (e != null) {
            Log.w(TAG, "onEvent:error", e)
            return
        }

        // Dispatch the event
        if (documentSnapshots != null) {
            for (change in documentSnapshots.documentChanges) {
                // snapshot of the changed document
                when (change.type) {
                    DocumentChange.Type.ADDED -> {
                        onDocumentAdded(change) // Add this line
                    }
                    DocumentChange.Type.MODIFIED -> {
                        onDocumentModified(change) // Add this line
                    }
                    DocumentChange.Type.REMOVED -> {
                        onDocumentRemoved(change) // Add this line
                    }
                }
            }
        }

        onDataChanged()
    }

Por último, implementa el método startListening() para adjuntar el objeto de escucha:

    fun startListening() {
        if (registration == null) {
            registration = query.addSnapshotListener(this)
        }
    }

Ahora la app está completamente configurada para leer datos desde Firestore. Vuelve a ejecutar la app. Deberías ver los restaurantes que agregaste en el paso anterior:

9e45f40faefce5d0.png

Ahora vuelve a la IU del emulador en tu navegador y edita uno de los nombres de los restaurantes. Deberías ver cómo cambia en la app casi al instante.

8. Cómo ordenar y filtrar datos

Actualmente, la app muestra los restaurantes mejor calificados en toda la colección, pero, en una app de restaurante real, el usuario querría ordenar y filtrar los datos. Por ejemplo, la app debería poder mostrar "Mejores marisquerías de Filadelfia" o "Pizzas menos costosas".

Si haces clic en la barra blanca en la parte superior de la app, aparecerá un diálogo de filtros. En esta sección, usaremos consultas de Firestore para que este diálogo funcione:

67898572a35672a5.png

Editemos el método onFilter() de MainFragment.kt. Este método acepta un objeto Filters, que es un objeto auxiliar que creamos para capturar el resultado del diálogo de filtros. Cambiaremos este método para construir una consulta a partir de los filtros:

    override fun onFilter(filters: Filters) {
        // Construct query basic query
        var query: Query = firestore.collection("restaurants")

        // Category (equality filter)
        if (filters.hasCategory()) {
            query = query.whereEqualTo(Restaurant.FIELD_CATEGORY, filters.category)
        }

        // City (equality filter)
        if (filters.hasCity()) {
            query = query.whereEqualTo(Restaurant.FIELD_CITY, filters.city)
        }

        // Price (equality filter)
        if (filters.hasPrice()) {
            query = query.whereEqualTo(Restaurant.FIELD_PRICE, filters.price)
        }

        // Sort by (orderBy with direction)
        if (filters.hasSortBy()) {
            query = query.orderBy(filters.sortBy.toString(), filters.sortDirection)
        }

        // Limit items
        query = query.limit(LIMIT.toLong())

        // Update the query
        adapter.setQuery(query)

        // Set header
        binding.textCurrentSearch.text = HtmlCompat.fromHtml(
            filters.getSearchDescription(requireContext()),
            HtmlCompat.FROM_HTML_MODE_LEGACY
        )
        binding.textCurrentSortBy.text = filters.getOrderDescription(requireContext())

        // Save filters
        viewModel.filters = filters
    }

En el fragmento anterior, compilamos un objeto Query adjuntando cláusulas where y orderBy para que coincidan con los filtros especificados.

Vuelve a ejecutar la app y selecciona el siguiente filtro para mostrar los restaurantes más populares de precios bajos:

7a67a8a400c80c50.png

Ahora deberías ver una lista filtrada de restaurantes que contiene solo opciones de precios bajos:

670188398c3c59.png

Si llegaste hasta aquí, ya compilaste una app de visualización de recomendaciones de restaurantes que funciona por completo en Firestore. Ahora puedes ordenar y filtrar restaurantes en tiempo real. En las próximas secciones, agregaremos opiniones a los restaurantes y reglas de seguridad a la app.

9. Organizar datos en subcolecciones

En esta sección, agregaremos calificaciones a la app para que los usuarios puedan opinar sobre sus restaurantes favoritos (o menos favoritos).

Colecciones y subcolecciones

Hasta ahora, almacenamos todos los datos de restaurantes en una colección de nivel superior llamada “restaurantes”. Cuando un usuario califica un restaurante, queremos agregar un nuevo objeto Rating a los restaurantes. Para esta tarea, usaremos una subcolección. Puedes considerar a una subcolección como una colección que se adjunta a un documento. Por lo tanto, cada documento de restaurante tendrá una subcolección de calificaciones llena de documentos de calificación. Las subcolecciones ayudan a organizar los datos sin sobredimensionar nuestros documentos ni requerir consultas complejas.

Para acceder a una subcolección, llama a .collection() en el documento principal:

val subRef = firestore.collection("restaurants")
        .document("abc123")
        .collection("ratings")

Puedes acceder a una subcolección y consultarla de la misma manera que en las colecciones de nivel superior; no existen limitaciones de tamaño ni cambios en el rendimiento. Puedes obtener más información sobre el modelo de datos de Firestore aquí.

Escribe datos en una transacción

Para agregar una Rating a la subcolección correspondiente, solo debes llamar a .add(), pero también necesitamos actualizar la calificación promedio del objeto Restaurant y la cantidad de calificaciones para reflejar los datos nuevos. Si usamos operaciones separadas para realizar estos dos cambios, hay varias condiciones de carrera que podrían generar datos obsoletos o incorrectos.

Para asegurarnos de que las calificaciones se agreguen correctamente, usaremos una transacción para agregar calificaciones a un restaurante. Esta transacción realizará algunas acciones:

  • Lee la calificación actual del restaurante y calcula la nueva
  • Agrega la calificación a la subcolección
  • Actualiza la calificación promedio y la cantidad de calificaciones del restaurante

Abre RestaurantDetailFragment.kt y, luego, implementa la función addRating:

    private fun addRating(restaurantRef: DocumentReference, rating: Rating): Task<Void> {
        // Create reference for new rating, for use inside the transaction
        val ratingRef = restaurantRef.collection("ratings").document()

        // In a transaction, add the new rating and update the aggregate totals
        return firestore.runTransaction { transaction ->
            val restaurant = transaction.get(restaurantRef).toObject<Restaurant>()
                ?: throw Exception("Restaurant not found at ${restaurantRef.path}")

            // Compute new number of ratings
            val newNumRatings = restaurant.numRatings + 1

            // Compute new average rating
            val oldRatingTotal = restaurant.avgRating * restaurant.numRatings
            val newAvgRating = (oldRatingTotal + rating.rating) / newNumRatings

            // Set new restaurant info
            restaurant.numRatings = newNumRatings
            restaurant.avgRating = newAvgRating

            // Commit to Firestore
            transaction.set(restaurantRef, restaurant)
            transaction.set(ratingRef, rating)

            null
        }
    }

La función addRating() muestra un Task que representa toda la transacción. En la función onRating(), se agregan objetos de escucha a la tarea para responder al resultado de la transacción.

Ahora vuelve a ejecutar la app y haz clic en uno de los restaurantes, lo que debería abrir la pantalla de detalles del restaurante. Haz clic en el botón + para comenzar a agregar una opinión. Para agregar una opinión, selecciona una cantidad de estrellas y, luego, ingresa texto.

78fa16cdf8ef435a.png

Si presionas Enviar, se iniciará la transacción. Cuando se complete la transacción, verás tu opinión a continuación y una actualización del recuento de opiniones del restaurante:

f9e670f40bd615b0.png

¡Felicitaciones! Ahora tienes una app social, local y móvil para opiniones sobre restaurantes que se creó en Cloud Firestore. Escuché que son muy populares en la actualidad.

10. Protege los datos

Hasta ahora, no hemos considerado la seguridad de esta aplicación. ¿Cómo sabemos que los usuarios solo pueden leer y escribir los propios datos correctos? Las bases de datos de Firestore están protegidas por un archivo de configuración llamado reglas de seguridad.

Abre el archivo firestore.rules. Deberías ver lo siguiente:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      //
      // WARNING: These rules are insecure! We will replace them with
      // more secure rules later in the codelab
      //
      allow read, write: if request.auth != null;
    }
  }
}

Cambiemos estas reglas para evitar cambios o accesos de datos no deseados. Ahora, abre el archivo firestore.rules y reemplaza el contenido por lo siguiente:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // Determine if the value of the field "key" is the same
    // before and after the request.
    function isUnchanged(key) {
      return (key in resource.data)
        && (key in request.resource.data)
        && (resource.data[key] == request.resource.data[key]);
    }

    // Restaurants
    match /restaurants/{restaurantId} {
      // Any signed-in user can read
      allow read: if request.auth != null;

      // Any signed-in user can create
      // WARNING: this rule is for demo purposes only!
      allow create: if request.auth != null;

      // Updates are allowed if no fields are added and name is unchanged
      allow update: if request.auth != null
                    && (request.resource.data.keys() == resource.data.keys())
                    && isUnchanged("name");

      // Deletes are not allowed.
      // Note: this is the default, there is no need to explicitly state this.
      allow delete: if false;

      // Ratings
      match /ratings/{ratingId} {
        // Any signed-in user can read
        allow read: if request.auth != null;

        // Any signed-in user can create if their uid matches the document
        allow create: if request.auth != null
                      && request.resource.data.userId == request.auth.uid;

        // Deletes and updates are not allowed (default)
        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, 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.

Para obtener más información sobre las reglas de seguridad, visita la documentación.

11. Conclusión

Ya creaste una app con todas las funciones sobre Firestore. Aprendiste sobre las funciones más importantes de Firestore, incluidas las siguientes:

  • Documentos y colecciones
  • Lectura y escritura de datos
  • Ordena y filtra con consultas
  • Subcolecciones
  • Transacciones

Más información

Si quieres seguir aprendiendo sobre Firestore, puedes comenzar por estos ejemplos:

La app de restaurante de este codelab se basó en la aplicación de ejemplo "Friendly Eats". Puedes explorar el código fuente de esa app aquí.

Implementa en producción (opcional)

Hasta ahora, esta app solo usó Firebase Emulator Suite. Si quieres aprender a implementar esta app en un proyecto real de Firebase, continúa con el siguiente paso.

12. Implementa tu app (opcional)

Hasta ahora, esta app es completamente local: todos los datos se encuentran en Firebase Emulator Suite. En esta sección, aprenderás a configurar tu proyecto de Firebase para que esta app funcione en producción.

Firebase Authentication

En Firebase console, ve a la sección Authentication y haz clic en Comenzar. Ve a la pestaña Método de acceso y selecciona la opción Correo electrónico/Contraseña en Proveedores nativos.

Habilita el método de acceso con Correo electrónico/Contraseña y haz clic en Guardar.

proveedores-de-acceso.png

Firestore

Crear base de datos

Navega a la sección Base de datos de Firestore de la consola y haz clic en Crear base de datos:

  1. Cuando se te pregunte si deseas que las reglas de seguridad decidan comenzar en el Modo de producción, las actualizaremos pronto.
  2. Elige la ubicación de la base de datos que deseas usar para tu app. Ten en cuenta que seleccionar la ubicación de la base de datos es una decisión permanente y, para cambiarla, tendrás que crear un proyecto nuevo. Si deseas obtener más información para elegir la ubicación de un proyecto, consulta la documentación.

Implementa reglas

Para implementar las reglas de seguridad que escribiste anteriormente, ejecuta el siguiente comando en el directorio del codelab:

$ firebase deploy --only firestore:rules

Esta acción implementará el contenido de firestore.rules en tu proyecto. Para confirmarlo, navega a la pestaña Reglas en la consola.

Implementa índices

La aplicación FriendlyEats tiene orden y filtrado complejos que requieren una serie de índices compuestos personalizados. Se pueden crear de forma manual en Firebase console, pero es más sencillo escribir sus definiciones en el archivo firestore.indexes.json y, luego, implementarlas con Firebase CLI.

Si abres el archivo firestore.indexes.json, verás que ya se proporcionaron los índices necesarios:

{
  "indexes": [
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "city", "mode": "ASCENDING" },
        { "fieldPath": "avgRating", "mode": "DESCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "category", "mode": "ASCENDING" },
        { "fieldPath": "avgRating", "mode": "DESCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "price", "mode": "ASCENDING" },
        { "fieldPath": "avgRating", "mode": "DESCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "city", "mode": "ASCENDING" },
        { "fieldPath": "numRatings", "mode": "DESCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "category", "mode": "ASCENDING" },
        { "fieldPath": "numRatings", "mode": "DESCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "price", "mode": "ASCENDING" },
        { "fieldPath": "numRatings", "mode": "DESCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "city", "mode": "ASCENDING" },
        { "fieldPath": "price", "mode": "ASCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "fields": [
        { "fieldPath": "category", "mode": "ASCENDING" },
        { "fieldPath": "price", "mode": "ASCENDING" }
      ]
    }
  ],
  "fieldOverrides": []
}

Para implementar estos índices, ejecuta el siguiente comando:

$ firebase deploy --only firestore:indexes

Ten en cuenta que la creación de índices no es instantánea. Puedes supervisar el progreso en Firebase console.

Configura la app

En los archivos util/FirestoreInitializer.kt y util/AuthInitializer.kt, configuramos el SDK de Firebase para que se conecte a los emuladores en el modo de depuración:

    override fun create(context: Context): FirebaseFirestore {
        val firestore = Firebase.firestore
        // Use emulators only in debug builds
        if (BuildConfig.DEBUG) {
            firestore.useEmulator(FIRESTORE_EMULATOR_HOST, FIRESTORE_EMULATOR_PORT)
        }
        return firestore
    }

Si quieres probar tu app con un proyecto real de Firebase, puedes hacer lo siguiente:

  1. Compila la app en modo de lanzamiento y ejecútala en un dispositivo.
  2. Reemplaza BuildConfig.DEBUG temporalmente por false y vuelve a ejecutar la app.

Ten en cuenta que es posible que debas salir de la app y volver a acceder para conectarte a producción.