Облако Firestore Android Codelab

Оптимизируйте свои подборки Сохраняйте и классифицируйте контент в соответствии со своими настройками.

1. Обзор

Цели

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

  • Чтение и запись данных в Firestore из приложения для Android
  • Слушайте изменения в данных Firestore в режиме реального времени
  • Используйте Firebase Authentication и правила безопасности для защиты данных Firestore
  • Написание сложных запросов Firestore

Предпосылки

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

  • Android Studio 4.0 или выше
  • Эмулятор Android с API 19 или выше.
  • Node.js версии 10 или выше
  • Java версии 8 или выше

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

  1. Войдите в консоль Firebase с помощью своей учетной записи Google.
  2. В консоли Firebase нажмите Добавить проект .
  3. Как показано на снимке экрана ниже, введите имя для своего проекта Firebase (например, «Friendly Eats») и нажмите « Продолжить ».

9d2f625aebcab6af.png

  1. Вас могут попросить включить Google Analytics, для целей этой кодовой лаборатории ваш выбор не имеет значения.
  2. Примерно через минуту ваш проект Firebase будет готов. Нажмите Продолжить .

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-студию. Нажмите « Файл» > « Создать » > «Импортировать проект» и выберите папку « friendlyeats-android ».

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

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

Установите интерфейс командной строки Firebase

Сначала вам нужно будет установить интерфейс командной строки Firebase . Если вы используете 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 , используйте --add, чтобы подключить ваш локальный проект к вашему проекту Firebase. Следуйте подсказкам, чтобы выбрать проект, который вы создали ранее, и, если вас попросят выбрать псевдоним, введите default .

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

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

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

В вашем терминале из каталога 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, которые мы используем для создания каждого документа ресторана.
  • Метод add() добавляет документ в коллекцию с автоматически сгенерированным идентификатором, поэтому нам не нужно было указывать уникальный идентификатор для каждого ресторана.

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

95691e9b71ba55e3.png

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

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

Эти данные на 100% локальны для вашей машины. Фактически, ваш реальный проект еще даже не содержит базы данных 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

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

а670188398c3c59.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 защищены файлом конфигурации под названием « Правила безопасности» .

Откройте файл firestore.rules , вы должны увидеть следующее:

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

Давайте изменим эти правила, чтобы предотвратить нежелательный доступ к данным или их изменение, откроем файл 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 перейдите в раздел « Аутентификация » и нажмите «Начать ». Перейдите на вкладку « Метод входа » и выберите параметр « Электронная почта/пароль » в разделе « Нативные поставщики ».

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

провайдеры входа.png

пожарный магазин

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

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

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

Развернуть правила

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

$ firebase deploy --only firestore:rules

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

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

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

Если вы откроете файл 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 и снова запустите приложение.

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