Cloud Firestore Android Codelab

1. Обзор

Цели

В этой лабораторной работе вы создадите приложение для рекомендации ресторанов на Android с поддержкой Cloud Firestore. Вы узнаете, как:

  • Чтение и запись данных в Firestore из приложения Android
  • Отслеживайте изменения данных Firestore в режиме реального времени
  • Используйте аутентификацию Firebase и правила безопасности для защиты данных Firestore
  • Написание сложных запросов Firestore

Предпосылки

Перед началом работы над этой лабораторной работой убедитесь, что у вас есть:

  • Android Studio Flamingo или новее
  • Эмулятор Android с API 19 или выше
  • Node.js версии 16 или выше
  • Java версии 17 или выше

2. Создайте проект Firebase

  1. Войдите в консоль Firebase, используя свою учетную запись Google.
  2. Нажмите кнопку, чтобы создать новый проект, а затем введите название проекта (например, FriendlyEats ).
  3. Нажмите «Продолжить» .
  4. При появлении соответствующего запроса ознакомьтесь с условиями Firebase и примите их, а затем нажмите кнопку «Продолжить» .
  5. (Необязательно) Включите помощь ИИ в консоли Firebase (так называемая «Gemini в Firebase»).
  6. Для этой лабораторной работы вам не понадобится Google Analytics, поэтому отключите опцию Google Analytics.
  7. Нажмите «Создать проект» , дождитесь завершения подготовки проекта, а затем нажмите «Продолжить» .

3. Настройте пример проекта

Загрузить код

Выполните следующую команду, чтобы клонировать пример кода для этой лабораторной работы. Это создаст на вашем компьютере папку friendlyeats-android :

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

Если на вашем компьютере нет git, вы также можете загрузить код непосредственно с GitHub.

Добавить конфигурацию Firebase

  1. В консоли Firebase выберите «Обзор проекта» в левой панели навигации. Нажмите кнопку Android , чтобы выбрать платформу. При запросе имени пакета используйте com.google.firebase.example.fireeats

73d151ed16016421.png

  1. Нажмите «Зарегистрировать приложение» и следуйте инструкциям, чтобы загрузить файл google-services.json и переместить его в папку app/ только что загруженного вами кода. Затем нажмите «Далее» .

Импортировать проект

Откройте Android Studio. Нажмите «Файл» > «Создать» > «Импортировать проект» и выберите папку friendlyeats-android .

4. Настройте эмуляторы Firebase

В этой лабораторной работе вы будете использовать Firebase Emulator Suite для локальной эмуляции Cloud Firestore и других сервисов Firebase. Это обеспечит безопасную, быструю и бесплатную локальную среду разработки для создания вашего приложения.

Установите Firebase CLI

Сначала вам необходимо установить Firebase CLI . Если вы используете macOS или Linux, выполните следующую команду cURL:

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

Если вы используете Windows, прочтите инструкции по установке , чтобы получить автономный двоичный файл или установить его через npm .

После установки CLI запуск firebase --version должен выдать версию 9.0.0 или выше:

$ firebase --version
9.0.0

Авторизоваться

Выполните firebase login , чтобы подключить CLI к вашей учётной записи Google. Откроется новое окно браузера для завершения процесса входа. Убедитесь, что вы выбрали ту же учётную запись, которую использовали при создании проекта Firebase ранее.

В папке friendlyeats-android выполните firebase use --add , чтобы подключить локальный проект к проекту Firebase. Следуя подсказкам, выберите созданный ранее проект и, если потребуется выбрать псевдоним, введите default .

5. Запустите приложение.

Теперь пришло время впервые запустить Firebase Emulator Suite и приложение FriendlyEats для Android.

Запустите эмуляторы

В терминале из каталога friendlyeats-android выполните firebase emulators:start , чтобы запустить эмуляторы Firebase. Вы должны увидеть следующие логи:

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

Теперь на вашем компьютере запущена полноценная локальная среда разработки! Не забудьте оставить эту команду работающей до конца работы над кодом: вашему Android-приложению потребуется подключиться к эмуляторам.

Подключите приложение к эмуляторам

Откройте файлы util/FirestoreInitializer.kt и util/AuthInitializer.kt в Android Studio. Эти файлы содержат логику подключения Firebase SDK к локальным эмуляторам, работающим на вашем компьютере, при запуске приложения.

В методе create() класса FirestoreInitializer изучите следующий фрагмент кода:

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

Мы используем BuildConfig , чтобы гарантировать подключение к эмуляторам только тогда, когда приложение работает в режиме debug . При компиляции приложения в режиме release это условие будет ложным.

Мы видим, что он использует метод useEmulator(host, port) для подключения Firebase SDK к локальному эмулятору Firestore. В ходе работы приложения мы будем использовать FirebaseUtil.getFirestore() для доступа к этому экземпляру FirebaseFirestore , чтобы гарантировать постоянное подключение к эмулятору Firestore при работе в режиме debug .

Запустите приложение

Если вы правильно добавили файл google-services.json , проект должен скомпилироваться. В Android Studio нажмите «Сборка» > «Пересобрать проект» и убедитесь, что ошибок больше нет.

Запустите приложение в Android Studio на эмуляторе Android. Сначала появится экран «Вход». Для входа в приложение можно использовать любой адрес электронной почты и пароль. Этот процесс входа подключается к эмулятору аутентификации Firebase, поэтому реальные учётные данные не передаются.

Теперь откройте интерфейс эмулятора, перейдя по адресу http://localhost:4000 в веб-браузере. Затем перейдите на вкладку «Аутентификация» , и вы увидите только что созданную учётную запись:

Эмулятор аутентификации Firebase

После завершения процесса входа вы увидите главный экран приложения:

de06424023ffb4b9.png

Скоро мы добавим некоторые данные для заполнения главного экрана.

6. Записать данные в Firestore

В этом разделе мы запишем некоторые данные в Firestore, чтобы иметь возможность заполнить в данный момент пустой домашний экран.

Основным объектом модели в нашем приложении является ресторан (см. model/Restaurant.kt ). Данные Firestore разделены на документы, коллекции и подколлекции. Мы будем хранить каждый ресторан как документ в коллекции верхнего уровня под названием "restaurants" . Чтобы узнать больше о модели данных Firestore, ознакомьтесь с документацией о документах и ​​коллекциях.

Для демонстрации мы добавим в приложение функцию создания десяти случайных ресторанов при нажатии кнопки «Добавить случайные пункты» в дополнительном меню. Откройте файл MainFragment.kt и замените содержимое метода onAddItemsClicked() следующим:

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

В приведенном выше коде есть несколько важных моментов, на которые следует обратить внимание:

  • Мы начали с получения ссылки на коллекцию "restaurants" . Коллекции создаются автоматически при добавлении документов, поэтому не было необходимости создавать коллекцию перед записью данных.
  • Документы можно создавать с использованием классов данных Kotlin, которые мы используем для создания каждого документа Restaurant.
  • Метод add() добавляет документ в коллекцию с автоматически сгенерированным идентификатором, поэтому нам не нужно было указывать уникальный идентификатор для каждого ресторана.

Теперь снова запустите приложение и нажмите кнопку «Добавить случайные элементы» в дополнительном меню (в правом верхнем углу), чтобы вызвать код, который вы только что написали:

95691e9b71ba55e3.png

Теперь откройте интерфейс эмулятора, перейдя по адресу http://localhost:4000 в веб-браузере. Затем перейдите на вкладку Firestore , и вы увидите только что добавленные данные:

Эмулятор аутентификации Firebase

Эти данные полностью локальны для вашего компьютера. Более того, ваш реальный проект пока даже не содержит базу данных Firestore! Это означает, что вы можете безопасно экспериментировать с изменением и удалением этих данных без каких-либо последствий.

Поздравляем, вы только что записали данные в Firestore! На следующем шаге мы узнаем, как отобразить эти данные в приложении.

7. Отображение данных из Firestore

На этом этапе мы научимся извлекать данные из Firestore и отображать их в нашем приложении. Первым шагом для чтения данных из Firestore является создание Query . Откройте файл MainFragment.kt и добавьте следующий код в начало метода onViewCreated() :

        // Firestore
        firestore = Firebase.firestore

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

Теперь мы хотим прослушивать запрос, чтобы получать все соответствующие документы и уведомления о будущих обновлениях в режиме реального времени. Поскольку наша конечная цель — привязать эти данные к RecyclerView , нам нужно создать класс RecyclerView.Adapter для прослушивания данных.

Откройте класс FirestoreAdapter , который уже частично реализован. Сначала реализуем EventListener в адаптере и определим функцию onEvent , чтобы он мог получать обновления запроса 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()
    }
    
    // ...
}

При первоначальной загрузке прослушиватель будет получать одно событие ADDED для каждого нового документа. По мере изменения результирующего набора запроса прослушиватель будет получать больше событий, содержащих изменения. Теперь давайте завершим реализацию прослушивателя. Сначала добавьте три новых метода: onDocumentAdded , onDocumentModified и 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)
    }

Затем вызовите эти новые методы из 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()
    }

Наконец реализуйте метод startListening() для присоединения прослушивателя:

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

Теперь приложение полностью настроено для чтения данных из Firestore. Запустите приложение ещё раз, и вы увидите рестораны, добавленные на предыдущем шаге:

9e45f40faefce5d0.png

Теперь вернитесь в интерфейс эмулятора в браузере и отредактируйте название одного из ресторанов. Вы увидите, как оно изменится в приложении практически мгновенно!

8. Сортировка и фильтрация данных

В настоящее время приложение отображает самые рейтинговые рестораны из всей коллекции, но в реальном приложении для ресторанов пользователю потребуется сортировать и фильтровать данные. Например, приложение должно отображать «Лучшие рестораны морепродуктов в Филадельфии» или «Самую дешёвую пиццу».

Нажатие на белую панель в верхней части приложения открывает диалоговое окно фильтров. В этом разделе мы воспользуемся запросами Firestore для реализации этого диалогового окна:

67898572a35672a5.png

Давайте отредактируем метод onFilter() в MainFragment.kt . Этот метод принимает объект Filters — вспомогательный объект, созданный нами для сбора данных диалогового окна фильтров. Мы изменим этот метод так, чтобы он создавал запрос на основе фильтров:

    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
    }

В приведенном выше фрагменте мы создаем объект Query , присоединяя предложения where и orderBy для соответствия заданным фильтрам.

Запустите приложение еще раз и выберите следующий фильтр, чтобы отобразить самые популярные недорогие рестораны:

7a67a8a400c80c50.png

Теперь вы должны увидеть отфильтрованный список ресторанов, содержащий только недорогие варианты:

a670188398c3c59.png

Если вы дошли до этого момента, значит, вы создали полноценное приложение для просмотра рекомендаций ресторанов на Firestore! Теперь вы можете сортировать и фильтровать рестораны в режиме реального времени. В следующих разделах мы добавим отзывы к ресторанам и правила безопасности в приложение.

9. Организуйте данные в подколлекциях

В этом разделе мы добавим в приложение рейтинги, чтобы пользователи могли оставлять отзывы о своих любимых (или нелюбимых) ресторанах.

Коллекции и подколлекции

До сих пор мы хранили все данные о ресторанах в коллекции верхнего уровня под названием «Рестораны». Когда пользователь оценивает ресторан, мы хотим добавить новый объект Rating к ресторанам. Для этой задачи мы будем использовать подколлекцию. Подколлекцию можно представить как коллекцию, прикреплённую к документу. Таким образом, каждый документ о ресторане будет иметь подколлекцию рейтингов, полную документов с рейтингами. Подколлекции помогают организовать данные, не перегружая документы и не требуя сложных запросов.

Чтобы получить доступ к подколлекции, вызовите .collection() в родительском документе:

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

Вы можете получать доступ к подколлекции и выполнять запросы к ней так же, как и к коллекции верхнего уровня, без ограничений по размеру и без снижения производительности. Подробнее о модели данных Firestore можно узнать здесь .

Запись данных в транзакции

Для добавления Rating в нужную подколлекцию достаточно вызвать метод .add() , но нам также необходимо обновить средний рейтинг и количество оценок объекта Restaurant , чтобы отразить новые данные. Если мы используем отдельные операции для внесения этих двух изменений, возникает ряд условий гонки, которые могут привести к устареванию или некорректности данных.

Чтобы обеспечить корректное добавление рейтингов, мы будем использовать транзакцию для добавления рейтингов ресторану. Эта транзакция выполнит несколько действий:

  • Ознакомьтесь с текущим рейтингом ресторана и рассчитайте новый.
  • Добавить оценку в подколлекцию
  • Обновите средний рейтинг ресторана и количество оценок.

Откройте RestaurantDetailFragment.kt и реализуйте функцию 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
        }
    }

Функция addRating() возвращает Task представляющую всю транзакцию. В функции onRating() к задаче добавляются слушатели, которые реагируют на результат транзакции.

Теперь снова запустите приложение и нажмите на один из ресторанов, чтобы открыть экран с информацией о нём. Нажмите кнопку «+» , чтобы начать добавлять отзыв. Чтобы добавить отзыв, выберите количество звёзд и введите текст.

78fa16cdf8ef435a.png

Нажатие кнопки «Отправить» запустит транзакцию. После завершения транзакции вы увидите свой отзыв ниже и обновлённое количество отзывов о ресторане:

f9e670f40bd615b0.png

Поздравляю! Теперь у вас есть социальное, локальное и мобильное приложение для отзывов о ресторанах, созданное на Cloud Firestore. Говорят, сейчас такие приложения очень популярны.

10. Защитите свои данные

До сих пор мы не рассматривали безопасность этого приложения. Откуда нам знать, что пользователи могут читать и записывать только свои собственные данные? Базы данных Firestore защищены конфигурационным файлом Security Rules .

Откройте файл firestore.rules и замените его содержимое следующим:

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

Эти правила ограничивают доступ, гарантируя, что клиенты будут вносить только безопасные изменения. Например, обновления документа ресторана могут изменить только рейтинги, но не название или другие неизменяемые данные. Рейтинги можно создавать только в том случае, если идентификатор пользователя совпадает с идентификатором вошедшего в систему пользователя, что предотвращает подмену.

Более подробную информацию о правилах безопасности можно найти в документации .

11. Заключение

Вы создали полнофункциональное приложение на базе Firestore. Вы узнали о самых важных функциях Firestore, включая:

  • Документы и коллекции
  • Чтение и запись данных
  • Сортировка и фильтрация с помощью запросов
  • Подколлекции
  • Транзакции

Узнать больше

Чтобы продолжить изучение Firestore, вот несколько хороших мест, с которых можно начать:

Приложение для ресторана в этой практической работе основано на примере приложения «Friendly Eats». Исходный код этого приложения можно посмотреть здесь .

Необязательно: Развертывание в производстве

До сих пор это приложение использовало только Firebase Emulator Suite. Если вы хотите узнать, как развернуть это приложение в реальном проекте Firebase, перейдите к следующему шагу.

12. (Необязательно) Разверните свое приложение.

До сих пор это приложение было полностью локальным, все данные хранились в Firebase Emulator Suite. В этом разделе вы узнаете, как настроить проект Firebase для работы этого приложения в продакшене.

Аутентификация Firebase

В консоли Firebase перейдите в раздел «Аутентификация» и нажмите « Начать» . Перейдите на вкладку «Способ входа» и выберите « Электронная почта/Пароль» в разделе «Нативные поставщики» .

Включите метод входа «Электронная почта/Пароль» и нажмите «Сохранить» .

sign-in-providers.png

Пожарный склад

Создать базу данных

Перейдите в раздел «База данных Firestore» на консоли и нажмите «Создать базу данных» :

  1. При появлении запроса на правила безопасности выберите запуск в производственном режиме . Мы скоро обновим эти правила.
  2. Выберите расположение базы данных, которое вы хотите использовать для своего приложения. Обратите внимание, что выбор расположения базы данных — это решение, которое не будет действовать , и для его изменения вам потребуется создать новый проект. Подробнее о выборе расположения проекта см. в документации .

Правила развертывания

Чтобы развернуть написанные ранее правила безопасности, выполните следующую команду в каталоге codelab:

$ firebase deploy --only firestore:rules

Это приведет к развертыванию содержимого firestore.rules в вашем проекте, в чем вы можете убедиться, перейдя на вкладку «Правила» в консоли.

Развертывание индексов

Приложение FriendlyEats использует сложную сортировку и фильтрацию, для которой требуется ряд пользовательских составных индексов. Их можно создать вручную в консоли Firebase, но проще записать их определения в файл firestore.indexes.json и развернуть с помощью Firebase CLI.

Если вы откроете файл firestore.indexes.json , то увидите, что необходимые индексы уже предоставлены:

{
  "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": []
}

Для развертывания этих индексов выполните следующую команду:

$ firebase deploy --only firestore:indexes

Обратите внимание, что создание индекса не происходит мгновенно. Вы можете отслеживать ход процесса в консоли Firebase.

Настройте приложение

В файлах util/FirestoreInitializer.kt и util/AuthInitializer.kt мы настроили Firebase SDK для подключения к эмуляторам в режиме отладки:

    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
    }

Если вы хотите протестировать свое приложение с реальным проектом Firebase, вы можете сделать следующее:

  1. Создайте приложение в режиме релиза и запустите его на устройстве.
  2. Временно замените BuildConfig.DEBUG на false и запустите приложение еще раз.

Обратите внимание, что для корректного подключения к производству вам может потребоваться выйти из приложения и войти в него снова.