マルチプラットフォーム Firestore Flutter

目標

この Codelab では、FlutterCloud Firestore を利用して、マルチプラットフォームのレストランおすすめアプリを構築します。

完成したアプリは、単一の Dart コードベースから、Android、iOS、ウェブで実行されます。

5e7215e72fa571b.png

学習内容

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

この Codelab で学びたいことは次のどれですか?

このトピックは初めてなので、簡単に概要を知りたい。 このトピックについてはある程度知っているが、復習したい。 プロジェクトで使用するサンプルコードを確認したい。 特定の項目に関する説明を確認したい。

必要なもの

Flutter または Firestore についてよく知らない場合は、最初に Flutter 用の Firebase Codelab を完了してください。

この Codelab を完了するには、以下が必要です。

  • 任意の IDE またはテキスト エディタ。例: Dart および Flutter プラグインで構成された Android Studio または VS Code
  • Google Chrome ブラウザ。および、Chrome DevTools に関するある程度の知識。
  • ウェブサポートが有効になっている Flutter の最新バージョン(beta チャンネル以降)。この Codelab ではウェブサポートを構成しますが、詳細を知るには Flutter のウェブサポートに関するページをご覧ください。
  • npm ツール。この Codelab の最後の部分(インデックスのデプロイデータの保護Firebase Hosting へのデプロイ)で、公式の firebase コマンドライン ツールをインストールするために使用します。
  • (任意)Android 向けにアプリをコンパイルする場合は、接続された Android デバイスまたはエミュレータ。
  • (任意)iOS 向けにアプリをコンパイルする場合は、Xcode の適正な最新バージョンを搭載した Mac。

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

  1. Firebase コンソールで [プロジェクトを追加] をクリックし、Firebase プロジェクトに FriendlyEat という名前を付けます。Firebase プロジェクトのプロジェクト ID を覚えておいてください(または、編集アイコンをクリックして、お好みのプロジェクト ID を設定してください)。
  2. [プロジェクトを作成] をクリックします。

構築するアプリでは、ウェブで利用できる以下の Firebase サービスを使用します。

  • Firebase Authentication: ユーザーを簡単に識別できます。
  • Cloud Firestore: 構造化データをクラウドに保存し、データが更新されたらすぐに通知を受け取れます。
  • Firebase Hosting: 静的アセットをホストして配信できます。

次に、Firebase コンソールを使用して、サービスを構成し、有効にする手順を実施します。

匿名認証を有効にする

認証はこの Codelab の主要なトピックではありませんが、アプリではなんらかの形の認証を行うことが重要です。ここでは、匿名ログイン(プロンプトが表示されず、ユーザーが自動的にログインする)を使用します。

匿名ログインを有効にするには:

  1. Firebase コンソールの左側のナビゲーション バーで、[開発] セクションを見つけます。
  2. [認証] をクリックし、[ログイン方法] タブをクリックします(または、Firebase コンソールに直接移動します)。
  3. [匿名] ログイン プロバイダを有効にし、[保存] をクリックします。

fee6c3ebdf904459.png

匿名ログインを有効にすると、ユーザーはウェブアプリにアクセスしたときに、アプリに自動的にログインできるようになります。詳しくは、匿名認証のドキュメントをご覧ください。

Cloud Firestore を有効にする

作成するアプリは、Cloud Firestore を使用してレストランの情報と評価を受信し、保存します。

Cloud Firestore を有効にするには:

  1. Firebase コンソールの [開発] セクションで [データベース] をクリックします。
  2. [Cloud Firestore] ペインで [データベースを作成] をクリックします。

57e83568e05c7710.png

  1. [テストモードで開始] をオンにし、セキュリティ ルールに関する免責条項を確認してから [有効にする] をクリックします。

テストモードでは、開発中に自由にデータベースに書き込むことができます。この Codelab の後半で、データベースのセキュリティを強化します。

daef1061fc25acc7.png

コマンドラインから、GitHub リポジトリのクローンを作成します。

git clone https://github.com/FirebaseExtended/codelab-friendlyeats-flutter.git friendlyeats-flutter

サンプルコードのクローンは、📁friendlyeats-flutter ディレクトリに作成する必要があります。これ以降は、次のディレクトリからコマンドを実行してください。

cd friendlyeats-flutter

スターター アプリをインポートする

📁friendlyeats-flutter ディレクトリを開くか、お好みの IDE にインポートします。このディレクトリには Codelab のスターター コードが格納されています。コードは、まだ機能しないレストランおすすめアプリで構成されています。

この Codelab 全体を通して、ディレクトリ内のコードを機能させるための編集を進めていきます。

作業対象のファイルを見つける

Flutter アプリの通常のエントリ ポイントはアプリの lib/main.dart ファイルですが、この Codelab ではデータの側面に焦点を当てます。

プロジェクト内で次のファイルを見つけます。

  • lib/src/model/data.dart: この Codelab で変更するメインファイル。Firestore からデータを読み書きする際のすべてのロジックが含まれています。
  • web/index.html: アプリを開始するためにブラウザが読み込むファイル。このファイルを変更して、ウェブ用の Firebase ライブラリをインストールし、初期化します。

Firebase CLI

Firebase コマンドライン インターフェース(CLI)を使用すると、プロジェクト内のファイルからウェブアプリと構成を Firebase に直接デプロイできます。

  1. 次の npm コマンドを実行して、CLI をインストールします。
npm -g install firebase-tools
  1. 次のコマンドを実行して、CLI が正しくインストールされたことを確認します。
firebase --version

Firebase CLI のバージョンが v7.4.0 以降であることを確認します。

  1. 次のコマンドを実行して、Firebase CLI を承認します。
firebase login

前の手順でクローンを作成したリポジトリには、すぐに使用できるプロジェクト構成(他の構成ファイルの場所、ホスティング デプロイメントなど)を含む firebase.json ファイルがすでに存在します。次に、アプリの作業用コピーを Firebase プロジェクトに関連付ける必要があります。

  1. コマンドラインがアプリのローカル ディレクトリにアクセスしていることを確認します。
  2. 次のコマンドを実行して、アプリを Firebase プロジェクトに関連付けます。
firebase use --add
  1. プロンプトが表示されたら、プロジェクト ID を選択して、Firebase プロジェクトにエイリアスを指定します。

エイリアスは、複数の環境(本番環境、ステージング環境など)を使用する場合に役立ちます。ただし、この Codelab では、default というエイリアスのみを使用します。

  1. コマンドラインで指示される手順に沿って操作します。

Flutter のウェブサポートを有効にする

Flutter アプリをコンパイルしてウェブで実行するには、この機能(現在はベータ版)を有効にする必要があります。ウェブサポートを有効にするには、次のコマンドを入力します。

$ flutter channel beta
$ flutter upgrade
$ flutter config --enable-web

IDE のデバイスのプルダウンか、または flutter devices を使用するコマンドラインで、Chromeウェブサーバーがリストに表示されるようになります。

Chrome デバイスでは自動的に Chrome が起動されます。ウェブサーバーではアプリをホストするサーバーが起動されるので、任意のブラウザからアプリを読み込むことができます。

開発中は DevTools を使用できるように Chrome デバイスを使用し、他のブラウザでテストする際はウェブサーバーを使用します。

Firebase プロジェクトを作成した後、その Firebase プロジェクトを使用するように 1 つ以上のアプリを構成できます。次のことを行います。

  • アプリのプラットフォーム固有の ID を Firebase に登録する。
  • アプリ用の構成ファイルを生成する。
  • プロジェクト フォルダ内の適切な場所に構成を追加する。

ac27fbbadff7a3b9.png

複数のプラットフォーム向けに Flutter アプリを開発する場合は、アプリが実行される各プラットフォームを同一の Firebase プロジェクト内で登録する必要があります。

この Codelab では、ウェブ プラットフォームに焦点を当てます。iOS と Android については、Flutter 用の Firebase Codelab で取り扱っているためです。Android または iOS のサポートを FriendlyEat アプリに追加する場合は、そちらの Codelab を参照してください。

Flutter アプリには、ウェブで実行する際にアプリのエントリ ポイントとして使用される特別な web/index.html ファイルがあります。そのエントリ ポイントをプロジェクト用の特定の構成で変更して、作成するウェブアプリが Firebase バックエンドに接続できるようにします。

ウェブ用に構成する

  1. Firebase コンソールの左側のナビゲーション バーで、[プロジェクトの概要] を選択し、[アプリに Firebase を追加して利用を開始しましょう] の下の [ウェブ] ボタンをクリックします。そうすると、以下のダイアログが表示されます。

f76ed55f71f15953.png

  1. アプリにニックネームを付けます。この値は、Firestore コンソールでアプリのウェブ バージョンを識別するために使用されます。
  2. [アプリの登録] をクリックします。
  3. アプリを登録した後、Firebase SDK を追加する手順により、Flutter アプリの web/index.html ファイルに貼り付ける必要があるコードが提供されます。貼り付けを完了すると、ファイルは次のようになります。

web/index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>friendlyeats</title>

  <!-- The core Firebase JS SDK is always required and must be listed first -->
  <script src="https://www.gstatic.com/firebasejs/7.5.1/firebase-app.js"></script>

  <!-- TODO: Add SDKs for Firebase products that you want to use
      https://firebase.google.com/docs/web/setup#available-libraries -->

  <script>
    // Your web app's Firebase configuration
    var firebaseConfig = {
      apiKey: "YoUr_RaNdOm_API_kEy",
      authDomain: "your-project-name.firebaseapp.com",
      databaseURL: "https://your-project-name.firebaseio.com",
      projectId: "your-project-name",
      storageBucket: "your-project-name.appspot.com",
      messagingSenderId: "012345678901",
      appId: "1:109876543210:web:r4nd0mH3xH45h"
    };
    // Initialize Firebase
    firebase.initializeApp(firebaseConfig);
  </script>

</head>
<body>
  <script src="main.dart.js" type="application/javascript"></script>
</body>
</html>
  1. 先ほど貼り付けたコードの中に TODO があることに注目してください。これを今から修正します。この Codelab では Firebase Authentication と Firestore を使用するので、以下のプロダクトのスクリプトタグを追加します。

web/index.html

  ...
  <!-- TODO: Add SDKs for Firebase products that you want to use
      https://firebase.google.com/docs/web/setup#available-libraries -->

  <script src="https://www.gstatic.com/firebasejs/7.5.1/firebase-auth.js"></script>
  <script src="https://www.gstatic.com/firebasejs/7.5.1/firebase-firestore.js"></script>

  <script>
    // Your web app's Firebase configuration
    var firebaseConfig = {
      ...
  1. web/index.html ファイルを保存し、[ウェブアプリに Firebase を追加] ダイアログで [コンソールに進む] をクリックします。
  2. これで、Flutter アプリを Firebase に接続する準備が整いました。

d43a19dbf1248134.png次のようなコンテンツができあがりました。

Firebase サポートを有効にするために必要なコード変更のほとんどは、作業中のプロジェクトにすでにチェックインされています。ただし、モバイル プラットフォームのサポートを追加するには、次のように、ウェブで行ったのと同様のプロセスを実施する必要があります。

  • 目的のプラットフォームを Firebase プロジェクトに登録する。
  • プラットフォーム固有の構成ファイルをダウンロードしてコードに追加する。

Flutter アプリの最上位ディレクトリには、iosandroid というサブディレクトリがあります。それぞれのディレクトリには、iOS と Android 用のプラットフォーム固有の構成ファイルが格納されています。

iOS を構成する

  1. Firebase コンソールの左側のナビゲーション バーで、[プロジェクトの概要] を選択し、[アプリに Firebase を追加して利用を開始しましょう] の下の [iOS] ボタンをクリックします。

以下のダイアログが表示されます。

c42139f18fb9a2ee.png

  1. 指定が必要な重要な値は、iOS バンドル ID です。バンドル ID を取得するには、次の 3 つの手順を実施します。
  1. コマンドライン ツールで、Flutter アプリの最上位ディレクトリに移動します。
  2. open ios/Runner.xcworkspace コマンドを実行して Xcode を開きます。
  1. Xcode で、左ペインの最上位にある [Runner] をクリックします。そうすると、右ペインに [General] タブが表示されます。[Bundle Identifier] の値をコピーします。

9733e26be329f329.png

  1. Firebase ダイアログに戻り、コピーしたバンドル ID を [iOS バンドル ID] フィールドに貼り付けて、[アプリの登録] をクリックします。
  1. 引き続き Firebase 内で、指示に沿って構成ファイル GoogleService-Info.plist をダウンロードします。
  2. Xcode に戻ります。[Runner] に [Runner] というサブフォルダがあることに注目してください(上記の画像を参照)。
  3. 先ほどダウンロードした GoogleService-Info.plist ファイルを [Runner] サブフォルダにドラッグします。
  4. Xcode にダイアログが表示されるので、[Finish] をクリックします。
  5. Firebase コンソールに戻ります。セットアップ手順で [次へ] をクリックし、残りの手順をスキップして Firebase コンソールのメインページに戻ります。

これで、iOS 向け Flutter アプリを構成する作業は完了です。

Android を構成する

  1. Firebase コンソールの左側のナビゲーション バーで、[プロジェクトの概要] を選択し、[アプリに Firebase を追加して利用を開始しましょう] の下の [Android] ボタンをクリックします。

以下のダイアログが表示されます。 8254fc299e82f528.png

  1. 指定が必要な重要な値は、Android パッケージ名 です。パッケージ名を取得するには、次の 2 つの手順を実施します。
  1. Flutter アプリのディレクトリで、ファイル android/app/src/main/AndroidManifest.xml を開きます。
  2. manifest 要素内にある package 属性の文字列値を確認します。この値が Android パッケージ名です(例: com.yourcompany.yourproject)。この値をコピーします。
  3. Firebase のダイアログで、コピーしたパッケージ名を [Android パッケージ名] フィールドに貼り付けます。
  4. この Codelab では、[デバッグ用の署名証明書 SHA-1] の指定は不要です。このフィールドを空白のままにします。
  5. [アプリの登録] をクリックします。
  6. 引き続き Firebase 内で、指示に沿って構成ファイル google-services.json をダウンロードします。
  7. Flutter アプリのディレクトリに移動し、先ほどダウンロードした google-services.json ファイルを android/app ディレクトリに移動します。
  8. Firebase コンソールに戻り、残りの手順をスキップして、Firebase コンソールのメインページに戻ります。
  9. すべての Gradle 構成がチェックインされています。アプリがまだ実行中の場合は、Gradle で依存関係をインストールできるように、アプリを閉じて再ビルドします。

これで、Android 向け Flutter アプリを構成する作業は完了です。

アプリで実際に作業を開始する準備が整いました。最初に、ローカルでアプリを実行します。構成した任意のプラットフォーム(デバイスとエミュレータが使用可能なもの)でアプリを実行できます。

次のコマンドを実行すると、使用可能なデバイスがわかります。

flutter devices

どのデバイスを使用できるかに応じて、上記のコマンドの出力は次のようになります。

3 connected devices:

Android SDK built for x86 • emulator-5554 • android-x86    • Android 7.1.1 (API 25) (emulator)
Chrome                    • chrome        • web-javascript • Google Chrome 79.0.3945.130
Web Server                • web-server    • web-javascript • Flutter Tools

この Codelab では、引き続き chrome デバイスを使用します。

  1. 次の Flutter CLI コマンドを実行します。
flutter run -d chrome
  1. Flutter は、ウェブ向けのアプリの作成を開始し、その後、実行中のアプリで Chrome ウィンドウを自動的に開きます。

これで、Firebase プロジェクトに接続された FriendlyEat のコピーが表示されるはずです。

アプリは自動的に Firebase プロジェクトに接続され、ユーザーは匿名ユーザーとして自動的にログインします。

c45806a2ac9300d9.png

このセクションでは、アプリの UI に入力されるデータを作成して、Cloud Firestore に書き込みます。これは Firebase コンソールを使用して手動で行うこともできますが、アプリで行うと、基本的な Cloud Firestore の書き込みのデモが表示されます。

データモデル

Firestore データは、コレクション、ドキュメント、フィールド、およびサブコレクションに分割されます。各レストランは、restaurants という名前の最上位のコレクションにドキュメントとして保存されます。

92f8dc2c769d2d6c.png

後で、各 restaurant 内にある ratings という名前のサブコレクションに各レビューを格納します。

a00d9eb006ddd6c0.png

レストランを Firestore に追加する

アプリ内のメインのモデル オブジェクトは、レストランです。次に、レストランのドキュメントを restaurants コレクションに追加するコードを作成します。

  1. lib/src/model/data.dart を開きます。
  2. 関数 addRestaurant を見つけます。
  3. 関数全体を次のコードに置き換えます。

lib/src/model/data.dart

Future<void> addRestaurant(Restaurant restaurant) {
  final restaurants = FirebaseFirestore.instance.collection('restaurants');
  return restaurants.add({
    'avgRating': restaurant.avgRating,
    'category': restaurant.category,
    'city': restaurant.city,
    'name': restaurant.name,
    'numRatings': restaurant.numRatings,
    'photo': restaurant.photo,
    'price': restaurant.price,
  });
}

上記のコードは、restaurants コレクションに新しいドキュメントを追加します。

そのためには、まず Cloud Firestore コレクション restaurants への参照を取得し、次にデータを adding します。

ドキュメント データは Restaurant オブジェクトから取得されます。このオブジェクトは、Firestore プラグイン用の Map に変換する必要があります。

レストランを追加する

  1. Flutter アプリを再ビルドして更新します(アプリを実行しているターミナル ウィンドウで Shift+R キーを押します)。
  2. [ADD SOME] をクリックします。

アプリは restaurants オブジェクトのランダムなセットを自動的に生成し、addRestaurant 関数を呼び出します。ただし、データの「取得」を実装する必要があるため(Codelab の次のセクションで行います)、ウェブアプリにデータは表示されません

Firebase コンソールで [開発] > [データベース] > [Cloud Firestore] タブに移動すると、restaurants コレクションに新しいドキュメントが表示されるはずです。

f06898b9d6dd4881.png

おつかれさまです。これで、ウェブアプリから Cloud Firestore にデータが書き込まれました。

次のセクションでは、Cloud Firestore からデータを取得してアプリに表示する方法を学びます。

このセクションでは、Cloud Firestore からデータを取得してアプリに表示する方法を学びます。主要なステップは、クエリの作成と、スナップショットの Stream のリッスンの 2 つです。このリスナーは、クエリと一致するすべての既存のデータの通知を受け取り、リアルタイムで更新を受信します。

最初に、デフォルトのフィルタされていないレストラン リストを表示するクエリを作成します。

  1. ファイル lib/src/model/data.dart に戻ります。
  2. 関数 loadAllRestaurants を見つけます。
  3. 関数全体を次のコードに置き換えます。

lib/src/model/data.dart

Stream<QuerySnapshot> loadAllRestaurants() {
  return FirebaseFirestore.instance
      .collection('restaurants')
      .orderBy('avgRating', descending: true)
      .limit(50)
      .snapshots();
}

上記のコードは、restaurants という名前の最上位のコレクションから最大 50 件のレストランを取得し、平均評価(現在はすべてゼロ)の順に並べ替えるクエリを作成します。

次に、Stream から返された各 QuerySnapshot を、レンダリングが可能な Restaurant データに変換する必要があります。

restaurants コレクションの QuerySnapshot から Restaurant 情報を抽出するには:

  1. ファイル lib/src/model/data.dart に戻ります。
  2. 関数 getRestaurantsFromQuery を見つけます。
  3. 関数全体を次のコードに置き換えます。

lib/src/model/data.dart

List<Restaurant> getRestaurantsFromQuery(QuerySnapshot snapshot) {
  return snapshot.docs.map((DocumentSnapshot doc) {
    return Restaurant.fromSnapshot(doc);
  }).toList();
}

前の手順で作成した Query の新しい QuerySnapshot が取得されるたびに、getRestaurantsFromQuery メソッドが呼び出されます。QuerySnapshots は、Firestore が Query への変更をリアルタイムでアプリに通知するために使用するメカニズムです。

このメソッドは、snapshot に含まれるすべての documents を、Flutter アプリの別の場所で使用できる Restaurant オブジェクトに変換するだけです。

両方のメソッドを実装した後は、アプリを再ビルドして再読み込みし、先ほど Firebase コンソールで見たレストランがアプリに表示されることを確認します。このセクションを正常に完了すると、アプリは Cloud Firestore でデータの読み書きを行います。

レストランのリストが変更されると、このリスナーは自動的に更新されます。Firebase コンソールに移動して、手動でレストランを削除したり、名前を変更したりしてみてください。変更内容がすぐにサイトに反映されるはずです。

edd9adbafa5bd539.png

ここまでは、onSnapshot を使用してリアルタイムで更新を取得する方法を学びました。しかし、いつでもそうするのが望ましいわけではありません。一度だけデータをフェッチすればよい場合もあります。

ユーザーがアプリで特定のレストランをクリックしたときに、特定のレストランをその ID から読み込む方法が必要です。

  1. ファイル lib/src/model/data.dart に戻ります。
  2. 関数 getRestaurant を見つけます。
  3. 関数全体を次のコードに置き換えます。

lib/src/model/data.dart

Future<Restaurant> getRestaurant(String restaurantId) {
  return FirebaseFirestore.instance
      .collection('restaurants')
      .doc(restaurantId)
      .get()
      .then((DocumentSnapshot doc) => Restaurant.fromSnapshot(doc));
}

このコードは、get() を使用して、リクエストされたレストランの情報を含む Future<DocumentSnapshot> を取得します。取得した情報は then() を通じて別の関数にパイプするだけです。準備ができた時点で、その関数で DocumentSnapshotRestaurant オブジェクトに変換します。

この方法を実装すると、各レストランの詳細ページを表示できます。

  1. flutter が実行されているターミナルで Shift+R キーを押して、アプリを更新します。
  2. リスト内のレストランをクリックすると、そのレストランの詳細ページが表示されます。

f8ca540dda5540a9.png

次のセクションでは、トランザクションを使用してレストランに評価を加えるために必要なコードを追加します。

このセクションでは、ユーザーがレストランに対するレビューを投稿する機能を追加します。これまでのところ、すべての書き込みはアトミックで、比較的単純でした。いずれかの書き込みがエラーになった場合は、単にユーザーに書き込みの再試行を求める方法が考えられます。ユーザーがそうしなかった場合、アプリは自動的に書き込みを再試行します。

アプリにはレストランのレビューを書き込みたいユーザーが多数いるはずなので、複数の読み取りと書き込みを調整する必要があります。最初に、レビューを送信する必要があります。次に、レストランの評価の countaverage rating を更新する必要があります。一方が失敗して他方が成功した場合、アプリは矛盾した状態に陥ります。つまり、データベースのある部分のデータが別の部分のデータと一致しなくなります。

幸いなことに、Cloud Firestore は単一のアトミック オペレーションで複数の読み取りと書き込みを実行できるトランザクション機能を備えているため、データの整合性を保証できます。

  1. ファイル lib/src/model/data.dart に戻ります。
  2. 関数 addReview を見つけます。
  3. 関数全体を次のコードに置き換えます。

lib/src/model/data.dart

Future<void> addReview({String restaurantId, Review review}) {
  final restaurant =
      FirebaseFirestore.instance.collection('restaurants').doc(restaurantId);
  final newReview = restaurant.collection('ratings').doc();

  return FirebaseFirestore.instance.runTransaction((Transaction transaction) {
    return transaction
        .get(restaurant)
        .then((DocumentSnapshot doc) => Restaurant.fromSnapshot(doc))
        .then((Restaurant fresh) {
      final newRatings = fresh.numRatings + 1;
      final newAverage =
          ((fresh.numRatings * fresh.avgRating) + review.rating) / newRatings;

      transaction.update(restaurant, {
        'numRatings': newRatings,
        'avgRating': newAverage,
      });

      transaction.set(newReview, {
        'rating': review.rating,
        'text': review.text,
        'userName': review.userName,
        'timestamp': review.timestamp ?? FieldValue.serverTimestamp(),
        'userId': review.userId,
      });
    });
  });
}

上記の関数は、restaurantId で表される Restaurantfresh バージョンをフェッチすることにより、開始トランザクションをトリガーします。

次に、restaurant ドキュメント参照内の avgRatingnumRatings の数値を更新します。

同時に、newReview ドキュメント参照を介して、レストランの ratings サブコレクションに新しい review を追加します。

追加したコードをテストするには:

  1. flutter が実行されているターミナルで Shift+R キーを押して、アプリを更新します。
  2. 任意のレストランの詳細ページに移動します。
  3. レビューをいくつか追加します。その方法は次のとおりです。
  • レビューがまだない場合は、空のリスト内で [ADD SOME] ボタンをクリックする
  • [+] フローティング操作ボタンをクリックして、レビューを入力する

現在、アプリにはレストランのリストが表示されますが、ユーザーが自分のニーズに基づいてフィルタすることはできません。このセクションでは、Cloud Firestore の高度なクエリを使用して、フィルタリングを有効にします。

Dim Sum のレストランをすべてフェッチするシンプルなクエリの例を次に示します。

Query filteredCollection = FirebaseFirestore.instance
        .collection('restaurants')
        .where('category', isEqualTo: 'Dim Sum');

where() メソッドは、その名が示すように、設定した制限を満たすフィールドを持つコレクションのメンバーのみをクエリしてダウンロードします。この例では、categoryDim Sum と等しいレストランのみをダウンロードします。

同様に、返されたデータを並べ替えることができます。

Query filteredAndSortedCollection = FirebaseFirestore.instance
        .collection('restaurants')
        .where('category', isEqualTo: 'Dim Sum')
        .orderBy('price', descending: true);

orderBy() メソッドを使用すると、クエリは Dim Sum のレストランを、price 属性値の降順に並べ替えます。

アプリのユーザーは、複数のフィルタを連結して、サンフランシスコのピザロサンゼルスのシーフードを人気順に表示する特定のクエリを作成できます。

ユーザーが選択した複数の条件に基づいてレストランをフィルタするクエリを構築するメソッドを作成します。

  1. ファイル lib/src/model/data.dart に戻ります。
  2. 関数 loadFilteredRestaurants を見つけます。
  3. 関数全体を次のコードに置き換えます。

lib/src/model/data.dart

Stream<QuerySnapshot> loadFilteredRestaurants(Filter filter) {
  Query collection = FirebaseFirestore.instance.collection('restaurants');
  if (filter.category != null) {
    collection = collection.where('category', isEqualTo: filter.category);
  }
  if (filter.city != null) {
    collection = collection.where('city', isEqualTo: filter.city);
  }
  if (filter.price != null) {
    collection = collection.where('price', isEqualTo: filter.price);
  }
  return collection
      .orderBy(filter.sort ?? 'avgRating', descending: true)
      .limit(50)
      .snapshots();
}

上記のコードは、複数の where フィルタと単一の orderBy 句を追加して、ユーザー入力に基づく複合クエリを作成します。これで、クエリはユーザーの要件を満たすレストランのみを返すようになりました。

flutter が実行されているターミナルで Shift+R キーを押して、ブラウザでアプリを更新します。

料金、都市、カテゴリでフィルタしてみてください。テスト中、ブラウザの JavaScript コンソールに次のようなエラーが表示されます。

The query requires an index. You can create it here: https://console.firebase.google.com/project/.../database/firestore/indexes?create_index=...

このようなエラーが発生するのは、Cloud Firestore はほとんどの複合クエリについてインデックスを必要とするためです。クエリでインデックスを必須にすると、Cloud Firestore は大規模なデータを高速で処理できます。

エラー メッセージからリンクを開くと、Firebase コンソールで、正しいパラメータが入力された状態のインデックス作成 UI が自動的に開きます。

次のセクションでは、このアプリに必要なインデックスを作成して、Firebase CLI から一度にデプロイします。

アプリ内のすべてのパスを調査して個々のインデックス作成リンクをたどりたくない場合は、Firebase CLI を使用して、一度に多数のインデックスを簡単にデプロイできます。

  1. アプリ プロジェクトのルートで、firestore.indexes.json ファイルを見つけます。

このファイルには、フィルタのすべての可能な組み合わせに対して必要なすべてのインデックスが記述されています。

firestore.indexes.json

{
 "indexes": [
   {
     "collectionGroup": "restaurants",
     "queryScope": "COLLECTION",
     "fields": [
       { "fieldPath": "city", "order": "ASCENDING" },
       { "fieldPath": "avgRating", "order": "DESCENDING" }
     ]
   },

   ...

 ]
}
  1. 次のコマンドを使用して、これらのインデックスをデプロイします。
firebase deploy --only firestore:indexes

数分後、インデックスが有効になり、エラー メッセージが表示されなくなります。インデックスの準備が完了する前にインデックスを使用しようとすると、次のようなエラーが表示されることがあります。

The query requires an index. That index is currently building and cannot be used yet. See its status here:
https://console.firebase.google.com/project/.../database/firestore/indexes?create_index=...

この Codelab の冒頭では、データベースをあらゆる読み取りまたは書き込みに対して完全にオープンにするアプリのセキュリティ ルールを設定しました。実際のアプリでは、望ましくないデータアクセスや変更を防ぐためのきめ細かいルールを設定します

  1. Firebase コンソールの [開発] セクションで [データベース] をクリックします。
  2. [Cloud Firestore] セクションの [ルール] タブをクリックします(または、Firebase コンソールに直接移動します)。
  3. デフォルト値を次のルールに置き換えて、[公開] をクリックします。

firestore.rules

service cloud.firestore {
  match /databases/{database}/documents {
    // Restaurants:
    //   - Authenticated user can read
    //   - Authenticated user can create/update (for demo)
    //   - Validate updates
    //   - Deletes are not allowed
    match /restaurants/{restaurantId} {
      allow read, create: if request.auth != null;
      allow update: if request.auth != null
                    && request.resource.data.name == resource.data.name
      allow delete: if false;

      // Ratings:
      //   - Authenticated user can read
      //   - Authenticated user can create if userId matches
      //   - Deletes and updates are not allowed
      match /ratings/{ratingId} {
        allow read: if request.auth != null;
        allow create: if request.auth != null
                      && request.resource.data.userId == request.auth.uid;
        allow update, delete: if false;
      }
    }
  }
}

これらのルールによりアクセスが制限され、クライアントは安全に変更を加えることができます。具体的には、次のようになります。

  • レストラン ドキュメントの更新では、評価のみが変更されます。名前およびその他の不変データは変更されません。
  • 評価は、ユーザー ID がログイン ユーザーと一致する場合にのみ作成できます。これにより、なりすましが防止されます。

Firebase コンソールの代わりに Firebase CLI を使用して、Firebase プロジェクトにルールをデプロイできます。作業ディレクトリの firestore.rules ファイルには、上記のルールがすでに含まれています。このルールを(Firebase コンソールを使用せずに)ローカル ファイルシステムからデプロイするには、次のコマンドを実行します。

firebase deploy --only firestore:rules

flutter build web

これまでは、Flutter アプリの「デバッグ」バージョンのみを使用していました。これらのビルドは、デバッグを容易にするための追加情報を含んでいるため、動作が若干低速です。

アプリをデプロイする前に、製品版(prod バージョン)をビルドする必要があります。Flutter では、build ツールを使用して製品版をビルドできます。

flutter build web

このツールは、製品版用にビルドされたすべてのアセットをプロジェクトの build/web ディレクトリに配置します。

これで、アプリを Firebase にデプロイする準備が整いました。

firebase deploy

プロジェクトは、Flutter が生成するアセットをビルドしてデプロイするようにあらかじめ構成されています(プロジェクトのルートにある firebase.json ファイルを確認してください)。

次のコマンドを使用して、アプリの新しいバージョンを Firebase にデプロイします。

firebase init hosting
firebase deploy --only hosting

上記のプロセスには数秒しかかかりません。以前のビルドをすべてクリーンアップし(flutter clean)、アプリを再ビルドして(flutter build web)、新たにビルドされたアセット(build/web のコンテンツ)を Firebase Hosting にデプロイします。

成功した場合のメッセージにはホスティング URL が含まれています。公開したアプリは、この URL を介してインターネット上で利用可能になります。

おつかれさまでした。

この Codelab では、Firebase Authentication と Firestore のプラグインを使用して Flutter ウェブアプリを Firebase に接続する方法を学び、Cloud Firestore での基本的な読み書きと高度な読み書きを実行し、セキュリティ ルールを使用してデータアクセスを保護しました。

完全な解答コードは、リポジトリの done ブランチにあります。

Dart と Flutter について詳しくは、それらの公式サイトをご覧ください。

Cloud Firestore について詳しくは、以下のリソースをご覧ください。

この Codelab は、他の Firestore(および Firebase)の機能を学ぶための出発点として最適です。もっとチャレンジしたい場合は、以下にトライしてみてください。

  • addRestaurantsBatch メソッドで一括書き込みを使用して、単一のリクエストですべてのレストランとレビューを追加し、アプリの UI が一度だけ更新されるようにします。
  • RestaurantAppBar ウィジェットを変更して、ユーザーがレビューを追加するとレストランの星評価がリアルタイムで更新されるようにします。
  • 現在はすべてのユーザーが匿名ユーザーであるため、google_sign_infirebase_auth を有効にして、レビューを投稿するユーザーの実際のファースト ネームを取得します。