Codelab do Cloud Firestore para Android

1. Visão geral

Gols

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

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

Pré-requisitos

Antes de iniciar este codelab, você precisa ter:

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

2. criar um projeto do 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 o nome do seu projeto do Firebase (por exemplo, "Friendly Eats") e clique em Continuar.

9d2f625aebcab6af.png

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

3. Configurar o projeto de amostra

Fazer o download do código

Execute o comando a seguir para clonar o exemplo de código deste codelab. Uma pasta chamada friendlyeats-android será criada na sua máquina:

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

Se você não tiver o git na máquina, faça o download do código diretamente no GitHub.

Adicionar a configuração do Firebase

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

73d151ed16016421.png

  1. Clique em Register App e siga as instruções para fazer o download do arquivo google-services.json e mova-o para a pasta app/ do código que você acabou de transferir por download. Depois, clique em Avançar.

Importar o projeto

Abra o Android Studio. Clique em File > New > Import Project e selecione a pasta Friendlyeats-android.

4. Configurar os emuladores do Firebase

Neste codelab, você usará o Pacote de emuladores do Firebase para emular localmente o Cloud Firestore e outros serviços do Firebase. Isso proporciona um ambiente de desenvolvimento local seguro, rápido e sem custos financeiros para criar seu app.

instalar a CLI do Firebase

Primeiro, você precisa instalar a CLI do Firebase. Se você estiver usando macOS ou Linux, execute o seguinte comando cURL:

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

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

Depois de instalar a CLI, a execução de firebase --version precisará informar uma versão de 9.0.0 ou mais recente:

$ firebase --version
9.0.0

Fazer login

Execute firebase login para conectar a CLI à sua Conta do Google. Uma nova janela do navegador será aberta para concluir o processo de login. Escolha a mesma conta que você usou ao criar seu projeto do Firebase.

Na pasta friendlyeats-android, execute firebase use --add para conectar seu projeto local ao do Firebase. Siga as instruções para selecionar o projeto que você criou anteriormente. Se for preciso escolher um alias, digite default.

5. Executar o aplicativo

Agora é hora de executar o Pacote de emuladores do Firebase e o app Android FriendlyEats pela primeira vez.

Executar os emuladores

No seu terminal, dentro do diretório friendlyeats-android, execute firebase emulators:start para iniciar os emuladores do Firebase. Você vai encontrar 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 em execução na sua máquina. Deixe esse comando em execução pelo restante do codelab. Seu app Android precisará se conectar aos emuladores.

Conectar o app 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 máquina, após a inicialização do aplicativo.

No método create() da classe FirestoreInitializer, examine esta parte do código:

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

Estamos usando o BuildConfig para garantir a conexão com os emuladores apenas quando o app estiver em execução no modo debug. Quando compilamos o app no modo release, essa condição é falsa.

Ele está usando o método useEmulator(host, port) para conectar o SDK do Firebase ao emulador local do Firestore. Em todo o app, vamos usar FirebaseUtil.getFirestore() para acessar essa instância de FirebaseFirestore. Assim, temos certeza de que estamos sempre nos conectando ao emulador do Firestore ao executar no modo debug.

Executar o aplicativo

Se você adicionou o arquivo google-services.json corretamente, o projeto vai ser compilado. No Android Studio, clique em Build > Rebuild Project e verifique se não há mais erros.

No Android Studio, execute o app no Android Emulator. Em primeiro lugar, a tela "Login" será exibida. Você pode usar qualquer e-mail e senha para fazer login no app. Esse processo de login se conecta ao emulador do Firebase Authentication. Portanto, nenhuma credencial real está sendo transmitida.

Agora abra a IU dos emuladores navegando até http://localhost:4000 no navegador da Web. Em seguida, clique na guia Authentication para ver a conta que acabou de criar:

Emulador do Firebase Auth

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

de06424023ffb4b9.png

Em breve, vamos adicionar alguns dados para preencher a tela inicial.

6. Gravar dados no Firestore

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

O principal objeto do modelo no nosso app é 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, vamos adicionar uma funcionalidade no app para criar 10 restaurantes aleatórios quando clicarmos no botão "Add Random Items" (Adicionar itens aleatórios) no menu flutuante. Abra o arquivo MainFragment.kt e substitua o conteúdo no 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)
        }
    }

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

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

Agora, execute o app novamente e clique no botão "Add Random Itens" 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 até http://localhost:4000 no navegador da Web. Em seguida, clique na guia Firestore para exibir os dados que acabou de adicionar:

Emulador do Firebase Auth

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

Parabéns, você acabou de gravar dados no Firestore. Na próxima etapa, vamos aprender a exibir esses dados no app.

7. Mostrar dados do Firestore

Nesta etapa, vamos aprender a recuperar dados do Firestore e mostrá-los no nosso app. A primeira etapa para ler dados do Firestore é criar um 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 receber todos os documentos correspondentes e ser notificados sobre atualizações futuras em tempo real. Como nosso objetivo final é vincular esses dados a um RecyclerView, precisamos criar uma classe RecyclerView.Adapter para detectá-los.

Abra a classe FirestoreAdapter, que já foi parcialmente implementada. Primeiro, vamos fazer com que o adaptador implemente EventListener e defina a função onEvent para que ele possa receber atualizações em 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 listener receberá um evento ADDED para cada novo documento. Conforme o conjunto de resultados da consulta mudar ao longo do tempo, o listener receberá mais eventos contendo as alterações. Agora vamos concluir a implementação do listener. 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 em 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 listener:

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

Agora, o app está totalmente configurado para ler dados do Firestore. Execute o app novamente para ver os restaurantes adicionados na etapa anterior:

9e45f40faefce5d0.png

Agora, volte para a interface do emulador no navegador e edite um dos nomes de restaurantes. Você verá a mudança no app quase instantaneamente.

8. Classificar e filtrar dados

O aplicativo atualmente exibe os restaurantes melhor avaliados em toda a coleção, mas, em um aplicativo de restaurante real, o usuário iria querer classificar e filtrar os dados. Por exemplo, o app precisa mostrar "Melhores restaurantes de frutos do mar na Filadélfia" ou "Pizza mais barata".

Clique na barra branca na parte superior do aplicativo para exibir uma caixa de diálogo de filtros. Nesta seção, usaremos as consultas do Firestore para fazer com que essa caixa de diálogo funcione:

67898572a35672a5.png

Vamos editar o método onFilter() de MainFragment.kt. Esse método aceita um objeto Filters, que é um objeto auxiliar que criamos para capturar a saída da caixa de diálogo de filtros. Mudaremos esse método para construir uma consulta usando os 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 snippet acima, criamos um objeto Query anexando as cláusulas where e orderBy para corresponder aos filtros fornecidos.

Execute o app novamente e selecione o filtro a seguir para mostrar os restaurantes mais baratos:

7a67a8a400c80c50.png

Você verá uma lista filtrada de restaurantes contendo apenas opções de preço baixo:

a670188398c3c59.png

Se você chegou até aqui, agora tem um app 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, vamos adicionar avaliações aos restaurantes e regras de segurança ao app.

9. Organizar dados em subcoleções

Nesta seção, adicionaremos notas ao app para que os usuários possam avaliar os restaurantes favoritos (ou menos favoritos) deles.

Coleções e subcoleções

Até agora, armazenamos todos os dados de restaurantes em uma coleção de nível superior chamada "restaurantes". Quando um usuário avalia um restaurante, queremos adicionar um novo objeto Rating a eles. Nesta tarefa, usaremos uma subcoleção. Pense em uma subcoleção como uma coleção anexada a um documento. Portanto, cada documento de restaurante terá uma subcoleção de classificações cheia de documentos de avaliaçã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")

É possível acessar e consultar uma subcoleção assim como acontece com uma coleção de nível superior. Não há limitações de tamanho nem mudanças de desempenho. Saiba mais sobre o modelo de dados do Firestore neste link.

Como gravar dados em uma transação

Para adicionar um Rating à subcoleção adequada é necessário apenas chamar .add(), mas também precisamos atualizar a nota média e o número de classificações do objeto Restaurant para refletir os novos dados. Se usarmos operações separadas para fazer essas duas alterações, haverá várias disputas que poderão resultar em dados desatualizados ou incorretos.

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

  • Ler a classificação atual do restaurante e calcular a nova avaliação
  • Adicionar a avaliação à subcoleção
  • Atualizar a nota 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 um Task que representa a transação inteira. Na função onRating(), os listeners são adicionados à tarefa para responder ao resultado da transação.

Agora, execute o app novamente e clique em um dos restaurantes, o que abrirá a tela de detalhes do restaurante. Clique no botão + para começar a adicionar um comentário. Para adicionar uma avaliação, escolha um número de estrelas e digite um texto.

78fa16cdf8ef435a.png.

Pressione Enviar para iniciar a transação. Quando a transação for concluída, sua avaliação será exibida abaixo e uma atualização na contagem de avaliações do restaurante:

f9e670f40bd615b0.png

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

10. Proteger seus dados

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

Abra o arquivo firestore.rules. Você vai 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 mudar essas regras para evitar acessos ou alterações de dados indesejados. 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;
      }
    }
  }
}

Essas regras restringem o acesso para garantir que os clientes só façam mudanças seguras. Por exemplo, as atualizações de um documento de restaurante podem alterar apenas as notas, não o nome ou 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 impede o spoofing.

Para ler mais sobre as regras de segurança, acesse a documentação.

11. Conclusão

Você criou um app com todos os recursos sobre o Firestore. Você aprendeu sobre os recursos mais importantes do Firestore, incluindo:

  • Documentos e coleções
  • Como ler e gravar dados
  • Classificar e filtrar com consultas
  • Subcoleções
  • Transações

Saiba mais

Para continuar aprendendo sobre o Firestore, veja como começar:

O app de restaurante neste codelab foi baseado no aplicativo de exemplo "Friendly Eats". Procure o código-fonte desse app aqui.

Opcional: implantar na produção

Até agora, este app usou apenas o Pacote de emuladores do Firebase. Se você quiser saber como implantar esse app em um projeto real do Firebase, continue para a próxima etapa.

12. Implantar o app (opcional)

Até agora, esse app é totalmente local. Todos os dados estão no Pacote de emuladores do Firebase. Nesta seção, você aprenderá a configurar seu projeto do Firebase para que o app funcione na produção.

Firebase Authentication

No Console do Firebase, acesse a seção Autenticação e clique em Começar. Acesse a guia Método de login e selecione a opção E-mail/senha em Provedores nativos.

Ative o método de login E-mail/senha e clique em Salvar.

sign-in-providers.png

Firestore

Criar banco de dados

Navegue até a seção Banco de dados do Firestore do console e clique em Criar banco de dados:

  1. Quando as regras de segurança forem solicitadas para começar no Modo de produção, atualizaremos essas regras em breve.
  2. Escolha o local do banco de dados que você quer usar no app. A seleção é uma decisão permanente. Se quiser mudar, será necessário criar um novo projeto. Para mais informações sobre como escolher um local do projeto, consulte a documentação.

Implantar regras

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

$ firebase deploy --only firestore:rules

Isso vai implantar o conteúdo de firestore.rules no seu projeto. Para confirmar isso, acesse a guia Rules no console.

Implantar índices

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

Se você abrir o arquivo firestore.indexes.json, vai notar 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

A criação do índice não é instantânea. Você pode monitorar o progresso no Console do Firebase.

Configure o app

Nos arquivos util/FirestoreInitializer.kt e util/AuthInitializer.kt, configuramos o SDK do Firebase para se conectar aos emuladores 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 você quiser testar o app com seu projeto real do Firebase, faça o seguinte:

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

Talvez seja necessário sair do app e fazer login novamente para estabelecer uma conexão adequada com a produção.