Cloud Firestore Android Codelab

1. 概要

目標

この Codelab では、Cloud Firestore を基盤とする Android でレストランのおすすめアプリを作成します。ここでは以下について学びます。

  • Android アプリから Firestore へのデータの読み取りと書き込み
  • Firestore データの変更をリアルタイムでリッスンする
  • Firebase Authentication とセキュリティ ルールを使用して Firestore データを保護する
  • 複雑な Firestore クエリを作成する

前提条件

この Codelab を開始する前に、以下が必要です。

  • Android Studio Flamingo 以降
  • API 19 以降を搭載した Android Emulator
  • Node.js バージョン 16 以降
  • Java バージョン 17 以降

2. Firebase プロジェクトを作成する

  1. Google アカウントで Firebase コンソールにログインします。
  2. Firebase コンソールで [プロジェクトを追加] をクリックします。
  3. 下のスクリーン キャプチャのように、Firebase プロジェクトの名前(「Friendly Eats」など)を入力し、[続行] をクリックします。

9d2f625aebcab6af.png

  1. この Codelab では、Google アナリティクスを有効にするよう求められる場合があります。ここでの選択は重要ではありません。
  2. 1 分ほどで Firebase プロジェクトの準備が整います。[続行] をクリックします。

3. サンプル プロジェクトを設定する

コードをダウンロードする

次のコマンドを実行して、この Codelab のサンプルコードのクローンを作成します。これにより、マシンに 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 を開きます。[File] > [New] > [Import Project] をクリックし、friendlyeats-android フォルダを選択します。

4. Firebase エミュレータを設定する

この Codelab では、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.

これで、お使いのマシン上で完全なローカル開発環境が動作するようになりました。Codelab の残りの部分では、このコマンドを実行したままにしてください。Android アプリはエミュレータに接続する必要があります。

アプリをエミュレータに接続する

Android Studio で util/FirestoreInitializer.kt ファイルと util/AuthInitializer.kt ファイルを開きます。これらのファイルには、アプリケーションの起動時に Firebase SDK をマシンで実行されているローカル エミュレータに接続するロジックが含まれています。

FirestoreInitializer クラスの create() メソッドで、次のコードを確認します。

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

BuildConfig を使用して、アプリが debug モードで実行されている場合にのみエミュレータに接続するようにします。アプリを release モードでコンパイルすると、この条件は false になります。

useEmulator(host, port) メソッドを使用して、Firebase SDK をローカル Firestore エミュレータに接続していることがわかります。アプリ全体を通して FirebaseUtil.getFirestore() を使用して FirebaseFirestore のインスタンスにアクセスするため、debug モードで実行されているときは常に Firestore エミュレータに接続しています。

アプリを実行する

google-services.json ファイルが適切に追加されていれば、プロジェクトはコンパイルされるはずです。Android Studio で [Build] > [Rebuild Project] をクリックし、エラーが残っていないことを確認します。

Android Studio の [Run] で、Android Emulator でアプリを実行します。最初に [ログイン] 画面が表示されます。アプリへのログインには任意のメールアドレスとパスワードを使用できます。このログイン プロセスは Firebase Authentication エミュレータに接続するため、実際の認証情報は送信されません。

ウェブブラウザで http://localhost:4000 に移動して、エミュレータ UI を開きます。次に [Authentication] タブをクリックすると、作成したアカウントが表示されます。

Firebase Auth エミュレータ

ログイン プロセスが完了すると、アプリのホーム画面が表示されます。

06424023ffb4b9.png

近日中に、ホーム画面に表示するデータを追加する予定です。

6. Firestore にデータを書き込む

このセクションでは、Firestore にデータを書き込み、現在空のホーム画面にデータを入力できるようにします。

アプリのメインのモデル オブジェクトはレストランです(model/Restaurant.kt を参照)。Firestore のデータはドキュメント、コレクション、サブコレクションに分割されています。各レストランを、"restaurants" というトップレベルのコレクションにドキュメントとして保存します。Firestore データモデルについて詳しくは、ドキュメントでドキュメントとコレクションの説明をご覧ください。

このデモでは、オーバーフロー メニューの [Add Random Places] ボタンをクリックすると、10 個の無作為のレストランを作成する機能をアプリに追加します。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() メソッドは、自動生成された ID を使用してドキュメントをコレクションに追加します。そのため、レストランごとに一意の ID を指定する必要はありません。

ここで、アプリを再度実行し、オーバーフロー メニュー(右上)の [Add Random Items] ボタンをクリックして、先ほど記述したコードを呼び出します。

95691e9b71ba55e3.png

ウェブブラウザで http://localhost:4000 に移動して、エミュレータ UI を開きます。次に、[Firestore] タブをクリックすると、先ほど追加したデータが表示されます。

Firebase Auth エミュレータ

このデータはマシンに対して 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()
    }
    
    // ...
}

初期読み込み時に、リスナーは新しいドキュメントごとに 1 つの ADDED イベントを受け取ります。クエリの結果セットは時間の経過とともに変化するため、リスナーは変更を含むイベントをさらに受け取ります。それでは、リスナーの実装を完了しましょう。まず、onDocumentAddedonDocumentModifiedonDocumentRemoved の 3 つの新しいメソッドを追加します。

    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

次に、ブラウザのエミュレータ UI に戻り、いずれかのレストラン名を編集します。ほぼ瞬時にアプリに表示されます。

8. データの並べ替えとフィルタリングを行う

このアプリは現在、コレクション全体で高評価のレストランを表示しますが、実際のレストラン アプリでは、ユーザーはデータの並べ替えとフィルタリングを行いたいと思うでしょう。たとえば、アプリで「フィラデルフィアで人気のシーフード レストラン」や「一番安いピザ」を表示できるようにします。

アプリの上部にある白いバーをクリックすると、フィルタ ダイアログが表示されます。このセクションでは、Firestore のクエリを使用して、このダイアログを機能させます。

67898572a35672a5.png

MainFragment.ktonFilter() メソッドを編集しましょう。このメソッドは、フィルタ ダイアログの出力をキャプチャするために作成したヘルパー オブジェクトである 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
    }

上記のスニペットでは、指定されたフィルタに一致する where 句と orderBy 句をアタッチして、Query オブジェクトを作成しています。

アプリを再度実行し、次のフィルタを選択して、特に人気の低価格レストランを表示します。

7a67a8a400c80c50.png

低価格オプションのみを含む、フィルタされたレストランのリストが表示されます。

a670188398c3c59.png

ここまでで、レストランのおすすめを表示するアプリを Firestore 上で完全に機能するようになりました。リアルタイムでレストランの並べ替えやフィルタができるようになりました。以降のセクションでは、レストランにレビューを追加し、アプリにセキュリティ ルールを追加します。

9. サブコレクションでデータを整理する

このセクションでは、アプリに評価を追加して、ユーザーがお気に入りのレストラン(または嫌いなレストラン)をレビューできるようにします。

コレクションとサブコレクション

ここまでで、すべてのレストラン データを「レストラン」という最上位のコレクションに保存してきました。ユーザーがレストランを評価する際は、新しい Rating オブジェクトをレストランに追加します。このタスクでは、サブコレクションを使用します。サブコレクションは、ドキュメントに関連付けられているコレクションと考えることができます。そのため、各レストラン ドキュメントには、評価ドキュメントが満載された ratings サブコレクションが作成されます。サブコレクションは、ドキュメントが肥大化したり、複雑なクエリを必要とすることなくデータを整理するのに役立ちます。

サブコレクションにアクセスするには、親ドキュメントに対して .collection() を呼び出します。

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

サブコレクションは、トップレベルのコレクションと同様にアクセスしてクエリを実行でき、サイズの制限やパフォーマンスの変更はありません。Firestore データモデルの詳細については、こちらをご覧ください。

トランザクションでのデータの書き込み

Rating を適切なサブコレクションに追加するには、.add() を呼び出すだけで済みますが、Restaurant オブジェクトの平均評価と評価数を更新して、新しいデータを反映する必要もあります。これら 2 つの変更を行うために別々のオペレーションを使用すると、多くの競合状態が発生し、データが最新でなかったり、正しくなかったりする可能性があります。

評価が正しく追加されるよう、トランザクションを使用してレストランに評価を追加します。このトランザクションでは次の処理が行われます。

  • レストランの現在の評価を読み、新しい評価を計算する
  • サブコレクションに評価を追加する
  • レストランの平均評価と評価数を更新する

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

これらのルールによりアクセスが制限され、クライアントが安全な変更のみを行うようになります。たとえば、レストランのドキュメントを更新すると、評価が変更されるだけで、名前やその他の不変データは変更されません。評価を作成できるのは、ユーザー ID がログインしているユーザーと一致する場合のみです。これにより、なりすましを防ぐことができます。

セキュリティ ルールの詳細については、こちらのドキュメントをご覧ください。

11. まとめ

これで、Firestore 上にフル機能のアプリが作成されました。このラボでは、Firestore で特に重要な次の機能について学習しました。

  • ドキュメントとコレクション
  • データの読み取りと書き込み
  • クエリを使用した並べ替えとフィルタリング
  • サブコレクション
  • 履歴

詳細

Firestore について詳しく知るには、まずはこちらをおすすめします。

この Codelab のレストラン アプリは、「Friendly Eats」サンプル アプリケーションをベースにしています。アプリのソースコードはこちらで確認できます。

省略可: 本番環境にデプロイする

これまでのところ、このアプリは Firebase Emulator Suite のみを使用しています。このアプリを実際の Firebase プロジェクトにデプロイする方法については、次のステップに進みます。

12. (省略可)アプリをデプロイする

これまでのところ、このアプリは完全にローカルであり、すべてのデータは Firebase Emulator Suite に含まれています。このセクションでは、このアプリが本番環境で動作するように Firebase プロジェクトを構成する方法を学びます。

Firebase Authentication

Firebase コンソールで [Authentication] セクションに移動し、[始める] をクリックします。[ログイン方法] タブに移動し、[ネイティブ プロバイダ] の [メール/パスワード] オプションを選択します。

ログイン方法として [メール/パスワード] を有効にして、[保存] をクリックします。

sign-in-providers.png

Firestore

データベースを作成

コンソールの [Firestore Database] セクションに移動し、[データベースを作成] をクリックします。

  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 に置き換えて、アプリを再度実行します。

本番環境に適切に接続するために、アプリからログアウトしてから再度ログインすることが必要になる場合があります。