Codelab do Cloud Firestore para Android

1. Visão Geral

Metas

Neste codelab, você criará um aplicativo de recomendação de restaurantes no Android com suporte do Cloud Firestore. Você vai aprender como:

  • Ler e gravar dados no Firestore a partir de um aplicativo Android
  • Ouça as alterações nos dados do Firestore em tempo real
  • Use o Firebase Authentication e as regras de segurança para proteger os dados do Firestore
  • Escreva consultas complexas do Firestore

Pré-requisitos

Antes de iniciar este codelab, verifique se você tem:

  • Android Studio Flamingo ou mais recente
  • Um emulador Android com API 19 ou superior
  • Node.js versão 16 ou superior
  • Java versão 17 ou superior

2. Crie um projeto Firebase

  1. Faça login no console do Firebase com sua conta do Google.
  2. No console do Firebase , clique em Adicionar projeto .
  3. Conforme mostrado na captura de tela abaixo, insira um nome para seu projeto Firebase (por exemplo, “Friendly Eats”) e clique em Continuar .

9d2f625aebcab6af.png

  1. Talvez seja necessário ativar o Google Analytics. Para os fins deste codelab, sua seleção não importa.
  2. Depois de mais ou menos um minuto, seu projeto Firebase estará pronto. Clique em Continuar .

3. Configure o projeto de amostra

Baixe o código

Execute o comando a seguir para clonar o código de amostra deste codelab. Isso criará uma pasta chamada friendlyeats-android em sua máquina:

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

Caso não tenha o git em sua máquina, você também pode baixar o código diretamente do GitHub.

Adicionar configuração do Firebase

  1. No console do Firebase , selecione Visão geral do projeto no painel de navegação esquerdo. Clique no botão Android para selecionar a plataforma. Quando for solicitado um nome de pacote, use com.google.firebase.example.fireeats

73d151ed16016421.png

  1. Clique em Registrar aplicativo e siga as instruções para baixar o arquivo google-services.json e mova-o para a pasta app/ do código que você acabou de baixar. Em seguida, clique em Avançar .

Importe o projeto

Abra o Android Studio. Clique em Arquivo > Novo > Importar Projeto e selecione a pasta friendlyeats-android .

4. Configure os emuladores Firebase

Neste codelab, você usará o Firebase Emulator Suite para emular localmente o Cloud Firestore e outros serviços do Firebase. Isso fornece um ambiente de desenvolvimento local seguro, rápido e gratuito para construir seu aplicativo.

Instale a CLI do Firebase

Primeiro você precisará instalar o Firebase CLI . Se estiver usando macOS ou Linux, você pode executar o seguinte comando cURL:

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

Se você estiver usando Windows, leia as instruções de instalação para obter um binário independente ou para instalar via npm .

Depois de instalar a CLI, executar firebase --version deverá reportar uma versão 9.0.0 ou superior:

$ firebase --version
9.0.0

Conecte-se

Execute firebase login para conectar a CLI à sua conta do Google. Isso abrirá uma nova janela do navegador para concluir o processo de login. Certifique-se de escolher a mesma conta que você usou ao criar seu projeto Firebase anteriormente.

De dentro da pasta friendlyeats-android execute firebase use --add para conectar seu projeto local ao seu projeto Firebase. Siga as instruções para selecionar o projeto que você criou anteriormente e, se for solicitado a escolher um alias, insira default .

5. Execute o aplicativo

Agora é hora de executar o Firebase Emulator Suite e o aplicativo FriendlyEats para Android pela primeira vez.

Execute os emuladores

Em seu terminal, no diretório friendlyeats-android execute firebase emulators:start para iniciar os Firebase Emulators. Você deverá ver registros como este:

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

Agora você tem um ambiente de desenvolvimento local completo rodando em sua máquina! Deixe esse comando em execução pelo resto do codelab. Seu app Android precisará se conectar aos emuladores.

Conecte o aplicativo aos emuladores

Abra os arquivos util/FirestoreInitializer.kt e util/AuthInitializer.kt no Android Studio. Esses arquivos contêm a lógica para conectar os SDKs do Firebase aos emuladores locais em execução na sua máquina, na inicialização do aplicativo.

No método create() da classe FirestoreInitializer , examine este trecho de código:

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

Estamos usando BuildConfig para garantir que só nos conectaremos aos emuladores quando nosso aplicativo estiver sendo executado no modo debug . Quando compilamos o aplicativo no modo release , esta condição será falsa.

Podemos ver que ele está usando o método useEmulator(host, port) para conectar o Firebase SDK ao emulador local do Firestore. Ao longo do aplicativo, usaremos FirebaseUtil.getFirestore() para acessar esta instância do FirebaseFirestore , portanto, temos certeza de que estaremos sempre nos conectando ao emulador do Firestore ao executar no modo de debug .

Execute o aplicativo

Se você adicionou o arquivo google-services.json corretamente, o projeto agora deve ser compilado. No Android Studio, clique em Build > Rebuild Project e certifique-se de que não haja erros restantes.

No Android Studio, execute o aplicativo em seu emulador Android. A princípio será apresentada uma tela de “Entrar”. Você pode usar qualquer e-mail e senha para entrar no aplicativo. Este processo de login está se conectando ao emulador do Firebase Authentication, portanto, nenhuma credencial real está sendo transmitida.

Agora abra a IU dos emuladores navegando para http://localhost:4000 em seu navegador da web. Em seguida, clique na guia Autenticação e você deverá ver a conta que acabou de criar:

Emulador de autenticação do Firebase

Depois de concluir o processo de login, você deverá ver a tela inicial do aplicativo:

de06424023ffb4b9.png

Em breve adicionaremos alguns dados para preencher a tela inicial.

6. Grave dados no Firestore

Nesta seção, gravaremos alguns dados no Firestore para que possamos preencher a tela inicial atualmente vazia.

O principal objeto do modelo em nosso aplicativo é um restaurante (consulte model/Restaurant.kt ). Os dados do Firestore são divididos em documentos, coleções e subcoleções. Armazenaremos cada restaurante como um documento em uma coleção de nível superior chamada "restaurants" . Para saber mais sobre o modelo de dados do Firestore, leia sobre documentos e coleções na documentação .

Para fins de demonstração, adicionaremos funcionalidade no aplicativo para criar dez restaurantes aleatórios quando clicarmos no botão "Adicionar itens aleatórios" no menu flutuante. Abra o arquivo MainFragment.kt e substitua o conteúdo do método onAddItemsClicked() por:

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

Existem algumas coisas importantes a serem observadas sobre o código acima:

  • Começámos por obter uma referência à colecção "restaurants" . As coleções são criadas implicitamente quando os documentos são adicionados, portanto não há necessidade de criar a coleção antes de gravar os dados.
  • Os documentos podem ser criados usando classes de dados Kotlin, que usamos para criar cada documento do Restaurante.
  • O método add() adiciona um documento a uma coleção com um ID gerado automaticamente, portanto não precisamos especificar um ID exclusivo para cada restaurante.

Agora execute o aplicativo novamente e clique no botão "Adicionar itens aleatórios" no menu flutuante (no canto superior direito) para invocar o código que você acabou de escrever:

95691e9b71ba55e3.png

Agora abra a IU dos emuladores navegando para http://localhost:4000 em seu navegador da web. Em seguida, clique na guia Firestore e você deverá ver os dados que acabou de adicionar:

Emulador de autenticação do Firebase

Esses dados são 100% locais em sua máquina. Na verdade, seu projeto real ainda nem contém um banco de dados Firestore! Isso significa que é seguro experimentar modificar e excluir esses dados sem consequências.

Parabéns, você acabou de gravar dados no Firestore! Na próxima etapa aprenderemos como exibir esses dados no aplicativo.

7. Exibir dados do Firestore

Nesta etapa aprenderemos como recuperar dados do Firestore e exibi-los em nosso aplicativo. A primeira etapa para ler os dados do Firestore é criar uma Query . Abra o arquivo MainFragment.kt e adicione o seguinte código ao início do 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())

Agora queremos ouvir a consulta, para obtermos todos os documentos correspondentes e sermos notificados sobre atualizações futuras em tempo real. Como nosso objetivo final é vincular esses dados a RecyclerView , precisamos criar uma classe RecyclerView.Adapter para ouvir os dados.

Abra a classe FirestoreAdapter , que já foi parcialmente implementada. Primeiro, vamos fazer o adaptador implementar EventListener e definir a função onEvent para que ele possa receber atualizações para uma consulta do 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()
    }
    
    // ...
}

No carregamento inicial, o ouvinte receberá um evento ADDED para cada novo documento. À medida que o conjunto de resultados da consulta muda ao longo do tempo, o ouvinte receberá mais eventos contendo as alterações. Agora vamos terminar de implementar o ouvinte. Primeiro adicione três novos métodos: onDocumentAdded , onDocumentModified e 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)
    }

Em seguida, chame estes novos métodos de 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 fim, implemente o método startListening() para anexar o ouvinte:

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

Agora o aplicativo está totalmente configurado para ler dados do Firestore. Execute o aplicativo novamente e você verá os restaurantes adicionados na etapa anterior:

9e45f40faefce5d0.png

Agora volte para a IU do emulador em seu navegador e edite um dos nomes de restaurante. Você deverá ver isso mudar no aplicativo quase instantaneamente!

8. Classifique e filtre dados

Atualmente, o aplicativo exibe os restaurantes mais bem avaliados de toda a coleção, mas em um aplicativo de restaurante real, o usuário desejaria classificar e filtrar os dados. Por exemplo, o aplicativo deve ser capaz de mostrar "Os melhores restaurantes de frutos do mar da Filadélfia" ou "Pizza mais barata".

Clicar na barra branca na parte superior do aplicativo abre uma caixa de diálogo de filtros. Nesta seção, usaremos consultas do Firestore para fazer esta caixa de diálogo funcionar:

67898572a35672a5.png

Vamos editar o método onFilter() de MainFragment.kt . Este método aceita um objeto Filters , que é um objeto auxiliar que criamos para capturar a saída da caixa de diálogo de filtros. Alteraremos este método para construir uma consulta a partir dos 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
    }

No trecho acima, construímos um objeto Query anexando as cláusulas where e orderBy para corresponder aos filtros fornecidos.

Execute o aplicativo novamente e selecione o seguinte filtro para mostrar os restaurantes de baixo preço mais populares:

7a67a8a400c80c50.png

Agora você deve ver uma lista filtrada de restaurantes contendo apenas opções de preços baixos:

a670188398c3c59.png

Se você chegou até aqui, agora criou um aplicativo de visualização de recomendações de restaurantes totalmente funcional no Firestore! Agora você pode classificar e filtrar restaurantes em tempo real. Nas próximas seções adicionaremos avaliações aos restaurantes e regras de segurança ao aplicativo.

9. Organize os dados em subcoleções

Nesta seção, adicionaremos classificações ao aplicativo para que os usuários possam avaliar seus restaurantes favoritos (ou menos favoritos).

Coleções e subcoleções

Até agora, armazenamos todos os dados dos restaurantes em uma coleção de nível superior chamada “restaurantes”. Quando um usuário avalia um restaurante, queremos adicionar um novo objeto Rating aos restaurantes. Para esta tarefa usaremos uma subcoleção. Você pode pensar em uma subcoleção como uma coleção anexada a um documento. Assim, cada documento de restaurante terá uma subcoleção de classificações repleta de documentos de classificação. As subcoleções ajudam a organizar os dados sem sobrecarregar nossos documentos ou exigir consultas complexas.

Para acessar uma subcoleção, chame .collection() no documento pai:

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

Você pode acessar e consultar uma subcoleção da mesma forma que faria com uma coleção de nível superior, não há limitações de tamanho ou alterações de desempenho. Você pode ler mais sobre o modelo de dados do Firestore aqui .

Gravando dados em uma transação

Adicionar uma Rating à subcoleção adequada requer apenas a chamada .add() , mas também precisamos atualizar a classificação média do objeto Restaurant e o número de classificações para refletir os novos dados. Se usarmos operações separadas para fazer essas duas alterações, haverá uma série de condições de corrida que podem resultar em dados obsoletos ou incorretos.

Para garantir que as classificações sejam adicionadas corretamente, usaremos uma transação para adicionar classificações a um restaurante. Esta transação realizará algumas ações:

  • Leia a classificação atual do restaurante e calcule a nova
  • Adicione a classificação à subcoleção
  • Atualizar a classificação média e o número de avaliações do restaurante

Abra RestaurantDetailFragment.kt e implemente a função 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
        }
    }

A função addRating() retorna uma Task que representa toda a transação. Na função onRating() , ouvintes são adicionados à tarefa para responder ao resultado da transação.

Agora execute o aplicativo novamente e clique em um dos restaurantes, o que deverá abrir a tela de detalhes do restaurante. Clique no botão + para começar a adicionar um comentário. Adicione um comentário escolhendo um número de estrelas e inserindo algum texto.

78fa16cdf8ef435a.png

Clicar em Enviar iniciará a transação. Quando a transação for concluída, você verá sua avaliação exibida abaixo e uma atualização na contagem de avaliações do restaurante:

f9e670f40bd615b0.png

Parabéns! Agora você tem um aplicativo social, local e móvel de avaliação de restaurantes criado no Cloud Firestore. Ouvi dizer que eles são muito populares hoje em dia.

10. Proteja seus dados

Até agora não consideramos a segurança deste aplicativo. Como sabemos que os usuários só podem ler e escrever os próprios dados corretos? Os bancos de dados Firestore são protegidos por um arquivo de configuração chamado Security Rules .

Abra o arquivo firestore.rules , você verá o seguinte:

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

Vamos alterar essas regras para evitar acessos ou alterações indesejadas aos dados, abra o arquivo firestore.rules e substitua o conteúdo pelo seguinte:

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 regras restringem o acesso para garantir que os clientes façam apenas alterações seguras. Por exemplo, as atualizações em um documento de restaurante só podem alterar as classificações, não o nome ou quaisquer outros dados imutáveis. As classificações só poderão ser criadas se o ID do usuário corresponder ao usuário conectado, o que evita falsificação.

Para ler mais sobre Regras de Segurança, visite a documentação .

11. Conclusão

Agora você criou um aplicativo completo no Firestore. Você aprendeu sobre os recursos mais importantes do Firestore, incluindo:

  • Documentos e coleções
  • Lendo e gravando dados
  • Classificando e filtrando com consultas
  • Subcoleções
  • Transações

Saber mais

Para continuar aprendendo sobre o Firestore, aqui estão alguns bons lugares para começar:

O app de restaurante neste codelab foi baseado no aplicativo de exemplo "Friendly Eats". Você pode navegar pelo código-fonte desse aplicativo aqui .

Opcional: implantar na produção

Até agora, este aplicativo usou apenas o Firebase Emulator Suite. Se você quiser aprender como implantar este aplicativo em um projeto real do Firebase, passe para a próxima etapa.

12. (Opcional) Implante seu aplicativo

Até agora, este aplicativo era totalmente local, todos os dados estão contidos no Firebase Emulator Suite. Nesta seção você aprenderá como configurar seu projeto Firebase para que este aplicativo funcione em produção.

Autenticação Firebase

No console do Firebase, vá para a seção Autenticação e clique em Primeiros passos . Navegue até a guia Método de login e selecione a opção E-mail/Senha em Provedores nativos .

Habilite o método de login por e-mail/senha e clique em Salvar .

provedores de login.png

Armazém de Fogo

Criar banco de dados

Navegue até a seção Firestore Database do console e clique em Create Database :

  1. Quando solicitado sobre regras de segurança, escolha iniciar no modo de produção , atualizaremos essas regras em breve.
  2. Escolha o local do banco de dados que você gostaria de usar para seu aplicativo. Observe que selecionar um local de banco de dados é uma decisão permanente e para alterá-la você terá que criar um novo projeto. Para obter mais informações sobre como escolher um local de projeto, consulte a documentação .

Implantar regras

Para implantar as regras de segurança que você escreveu anteriormente, execute o seguinte comando no diretório codelab:

$ firebase deploy --only firestore:rules

Isso implantará o conteúdo de firestore.rules em seu projeto, o que você pode confirmar navegando até a guia Regras no console.

Implantar índices

O aplicativo FriendlyEats possui classificação e filtragem complexas que requerem vários índices compostos personalizados. Eles podem ser criados manualmente no console do Firebase, mas é mais simples escrever suas definições no arquivo firestore.indexes.json e implantá-las usando a CLI do Firebase.

Se você abrir o arquivo firestore.indexes.json , verá que os índices necessários já foram fornecidos:

{
  "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 implantar esses índices, execute o seguinte comando:

$ firebase deploy --only firestore:indexes

Observe que a criação do índice não é instantânea, você pode monitorar o progresso no console do Firebase.

Configurar o aplicativo

Nos arquivos util/FirestoreInitializer.kt e util/AuthInitializer.kt configuramos o SDK do Firebase para se conectar aos emuladores quando estiver no modo de depuração:

    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
    }

Se quiser testar seu aplicativo com seu projeto real do Firebase, você pode:

  1. Crie o aplicativo no modo de lançamento e execute-o em um dispositivo.
  2. Substitua temporariamente BuildConfig.DEBUG por false e execute o aplicativo novamente.

Observe que pode ser necessário sair do aplicativo e fazer login novamente para se conectar corretamente à produção.