FlutterのFirebaseを知る

1.始める前に

このコードラボでは、AndroidとiOS向けのFlutterモバイルアプリを作成するためのFirebaseの基本のいくつかを学びます。

前提条件

このコードラボは、 Flutterに精通しており、FlutterSDKとエディターがインストールされていることを前提としています。

作成するもの

このコードラボでは、Flutterを使用して、Android、iOS、Web、およびmacOSでイベントRSVPおよびゲストブックチャットアプリを構築します。 Firebase認証でユーザーを認証し、CloudFirestoreを使用してデータを同期します。

必要なもの

このコードラボは、次のデバイスのいずれかを使用して実行できます。

  • コンピューターに接続され、開発者モードに設定されている物理デバイス(AndroidまたはiOS)。
  • iOSシミュレーター。 ( Xcodeツールをインストールする必要があります。)
  • Androidエミュレーター。 ( Android Studioでのセットアップが必要です。)

上記に加えて、次のものも必要になります。

  • Chromeなどの選択したブラウザ。
  • DartおよびFlutterプラグインで構成されたAndroidStudioVSCodeなど、選択したIDEまたはテキストエディター。
  • Flutterの最新のstableバージョン(または、エッジでの生活を楽しんでいる場合はbeta版)。
  • Firebaseプロジェクトを作成および管理するためのGmailアカウントなどのGoogleアカウント。
  • Gmailアカウントにログインしたfirebaseコマンドラインツール
  • codelabのサンプルコード。コードを取得する方法については、次の手順を参照してください。

2.サンプルコードを取得します

GitHubからプロジェクトの初期バージョンをダウンロードすることから始めましょう。

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

git clone https://github.com/flutter/codelabs.git flutter-codelabs

または、 GitHubのCLIツールがインストールされている場合:

gh repo clone flutter/codelabs flutter-codelabs

サンプルコードは、codelabsのコレクションのコードが含まれているflutter-codelabsディレクトリに複製する必要があります。このコードラボのコードは、 flutter-codelabs/firebase-get-to-know-flutterます。

flutter-codelabs/firebase-get-to-know-flutter下のディレクトリ構造は、名前が付けられた各ステップの最後にあるべき場所の一連のスナップショットです。これはステップ2であるため、一致するファイルを見つけるのは次のように簡単です。

cd flutter-codelabs/firebase-get-to-know-flutter/step_02

先にスキップしたい場合、またはステップの後に何かがどのように見えるかを確認したい場合は、関心のあるステップにちなんで名付けられたディレクトリを調べてください。

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

flutter-codelabs/firebase-get-to-know-flutter/step_02ディレクトリを開くかインポートします。このディレクトリには、まだ機能していないFlutterミートアップアプリで構成されるcodelabの開始コードが含まれています。

作業するファイルを見つけます

このアプリのコードは複数のディレクトリに分散しています。この機能の分割は、コードを機能ごとにグループ化することにより、作業を容易にするように設計されています。

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

  • lib/main.dart :このファイルには、メインのエントリポイントとアプリケーションウィジェットが含まれています。
  • lib/src/widgets.dart :このファイルには、アプリケーションのスタイルを標準化するのに役立つウィジェットがいくつか含まれています。これらは、スターターアプリの画面を構成するために使用されます。
  • lib/src/authentication.dart :このファイルには、Firebaseメールベース認証のログインユーザーエクスペリエンスを作成するための一連のウィジェットを備えたFirebaseUIAuthの部分的な実装が含まれています。認証フロー用のこれらのウィジェットは、スターターアプリではまだ使用されていませんが、すぐに接続します。

アプリケーションの残りの部分を構築するために、必要に応じてファイルを追加します。

lib/main.dartファイルを確認する

このアプリはgoogle_fontsパッケージを利用して、アプリ全体でRobotoをデフォルトのフォントにすることができます。やる気のある読者のための演習は、 fonts.google.comを探索し、アプリのさまざまな部分で見つけたフォントを使用することです。

lib/src/widgets.dartのヘルパーウィジェットをHeaderParagraphIconAndDetailの形式で利用しています。これらのウィジェットは、重複するコードを排除することにより、 HomePageで説明されているページレイアウトの乱雑さを軽減します。これには、一貫したルックアンドフィールを可能にするという追加の利点があります。

Android、iOS、Web、macOSでのアプリの外観は次のとおりです。

アプリのプレビュー

3.Firebaseプロジェクトを作成して設定します

イベント情報を表示することはゲストにとっては素晴らしいことですが、イベントを表示するだけでは誰にとってもあまり役に立ちません。このアプリにいくつかの動的機能を追加しましょう。このためには、Firebaseをアプリに接続する必要があります。 Firebaseの使用を開始するには、Firebaseプロジェクトを作成して設定する必要があります。

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

  1. Firebaseにログインします。
  2. Firebaseコンソールで、[プロジェクトの追加](または[プロジェクトの作成])をクリックし、FirebaseプロジェクトにFirebase-Flutter-Codelabという名前を付けます。

4395e4e67c08043a.png

  1. プロジェクト作成オプションをクリックします。プロンプトが表示されたら、Firebaseの利用規約に同意します。このアプリではアナリティクスを使用しないため、Googleアナリティクスの設定はスキップしてください。

b7138cde5f2c7b61.png

Firebaseプロジェクトの詳細については、 「Firebaseプロジェクトについて理解する」をご覧ください。

作成しているアプリは、ウェブアプリで利用できるいくつかのFirebase製品を使用しています。

  • ユーザーがアプリにログインできるようにするFirebase認証
  • 構造化データをクラウドに保存し、データが変更されたときに即座に通知を受け取るCloudFirestore。
  • データベースを保護するためのFirebaseセキュリティルール

これらの製品の一部は、特別な設定が必要であるか、Firebaseコンソールを使用して有効にする必要があります。

Firebase認証のメールログインを有効にする

ユーザーがWebアプリにサインインできるようにするには、このコードラボで電子メール/パスワードのサインイン方法を使用します。

  1. Firebaseコンソールで、左側のパネルの[ビルド]メニューを展開します。
  2. [認証]をクリックし、[開始]ボタンをクリックしてから、[サインイン方法]タブをクリックします(または、ここをクリックして[サインイン方法]タブに直接移動します)。
  3. [サインインプロバイダー]リストで[電子メール/パスワード]をクリックし、[有効にする]スイッチをオンの位置に設定して、[保存]をクリックします。 58e3e3e23c2f16a4.png

CloudFirestoreを有効にする

Webアプリは、 Cloud Firestoreを使用してチャットメッセージを保存し、新しいチャットメッセージを受信します。

CloudFirestoreを有効にする:

  1. Firebaseコンソールの[ビルド]セクションで、[ CloudFirestore ]をクリックします。
  2. [データベースの作成]をクリックします。 99e8429832d23fa3.png
  1. [テストモードで開始]オプションを選択します。セキュリティルールに関する免責事項をお読みください。テストモードでは、開発中にデータベースに自由に書き込むことができます。 [次へ]をクリックします。 6be00e26c72ea032.png
  1. データベースの場所を選択します(デフォルトを使用できます)。この場所は後で変更できないことに注意してください。 278656eefcfb0216.png
  2. [有効にする]をクリックします。

4.Firebaseの構成

FlutterでFirebaseを使用するには、FlutterFireライブラリを正しく利用するようにFlutterプロジェクトを構成するプロセスに従う必要があります。

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

Flutterアプリのトップレベルのディレクトリには、 androidiosmacoswebというサブディレクトリがあります。これらのディレクトリは、それぞれiOSとAndroidのプラットフォーム固有の構成ファイルを保持します。

依存関係を構成する

このアプリで使用している2つのFirebase製品(FirebaseAuthとCloudFirestore)のFlutterFireライブラリを追加する必要があります。次の3つのコマンドを実行して、依存関係を追加します。

$ flutter pub add firebase_core 
Resolving dependencies...
+ firebase_core 1.10.5
+ firebase_core_platform_interface 4.2.2
+ firebase_core_web 1.5.2
+ flutter_web_plugins 0.0.0 from sdk flutter
+ js 0.6.3
  test_api 0.4.3 (0.4.8 available)
Changed 5 dependencies!

firebase_coreは、すべてのFirebaseFlutterプラグインに必要な共通コードです。

$ flutter pub add firebase_auth
Resolving dependencies...
+ firebase_auth 3.3.3
+ firebase_auth_platform_interface 6.1.8
+ firebase_auth_web 3.3.4
+ intl 0.17.0
  test_api 0.4.3 (0.4.8 available)
Changed 4 dependencies!

firebase_authは、Firebaseの認証機能との統合を可能にします。

$ flutter pub add cloud_firestore
Resolving dependencies...
+ cloud_firestore 3.1.4
+ cloud_firestore_platform_interface 5.4.9
+ cloud_firestore_web 2.6.4
  test_api 0.4.3 (0.4.8 available)
Changed 3 dependencies!

cloud_firestoreは、CloudFirestoreデータストレージへのアクセスを有効にします。

$ flutter pub add provider
Resolving dependencies...
+ nested 1.0.0
+ provider 6.0.1
  test_api 0.4.3 (0.4.8 available)
Changed 2 dependencies!

必要なパッケージを追加すると同時に、Firebaseを適切に利用するようにiOS、Android、macOS、およびWebランナープロジェクトを構成する必要があります。また、ビジネスロジックを表示ロジックから分離できるようにするproviderパッケージも使用しています。

flutterfireインストール

FlutterFire CLIは、基盤となるFirebaseCLIに依存します。まだ行っていない場合は、 FirebaseCLIがマシンにインストールされていることを確認してください。

次に、次のコマンドを実行してFlutterFireCLIをインストールします。

$ dart pub global activate flutterfire_cli

インストールすると、 flutterfireコマンドはグローバルに使用できるようになります。

アプリの構成

CLIは、Firebaseプロジェクトと選択したプロジェクトアプリケーションから情報を抽出して、特定のプラットフォームのすべての構成を生成します。

アプリケーションのルートで、configureコマンドを実行します。

$ flutterfire configure

構成コマンドは、いくつかのプロセスをガイドします。

  1. Firebaseプロジェクトの選択(.firebasercファイルまたはFirebaseコンソールから)。
  2. 構成するプラットフォーム(Android、iOS、macOS、Webなど)を入力します。
  3. 選択したプラットフォームのどのFirebaseアプリケーションを使用して構成を抽出する必要があるかを特定します。デフォルトでは、CLIは現在のプロジェクト構成に基づいてFirebaseアプリを自動的に照合しようとします。
  4. プロジェクトでfirebase_options.dartファイルを生成します。

macOSを構成する

macOSのFlutterは、完全にサンドボックス化されたアプリケーションを構築します。このアプリケーションはネットワークを使用してFirebaseサーバーと通信するために統合されているため、ネットワーククライアント権限を使用してアプリケーションを構成する必要があります。

macos / Runner / DebugProfile.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.app-sandbox</key>
	<true/>
	<key>com.apple.security.cs.allow-jit</key>
	<true/>
	<key>com.apple.security.network.server</key>
	<true/>
  <!-- Add the following two lines -->
	<key>com.apple.security.network.client</key>
	<true/>
</dict>
</plist>

macos / Runner / Release.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.app-sandbox</key>
	<true/>
  <!-- Add the following two lines -->
	<key>com.apple.security.network.client</key>
	<true/>
</dict>
</plist>

詳細については、エンタイトルメントとアプリサンドボックスを参照してください。

5.ユーザーサインイン(RSVP)を追加します

アプリにFirebaseを追加したので、 Firebase認証を使用してユーザーを登録するRSVPボタンを設定できます。 Androidネイティブ、iOSネイティブ、およびWebの場合、事前にビルドされたFirebaseUI Authパッケージがありますが、Flutterの場合、この機能をビルドする必要があります。

手順2で取得したプロジェクトには、ほとんどの認証フローのユーザーインターフェイスを実装する一連のウィジェットが含まれていました。 FirebaseAuthenticationをアプリケーションに統合するためのビジネスロジックを実装します。

プロバイダーとのビジネスロジック

providerパッケージを使用して、アプリケーションのFlutterウィジェットのツリー全体で一元化されたアプリケーション状態オブジェクトを使用できるようにします。まず、 lib/main.dartの上部にあるインポートを変更します。

lib / main.dart

import 'package:firebase_auth/firebase_auth.dart'; // new
import 'package:firebase_core/firebase_core.dart'; // new
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';           // new

import 'firebase_options.dart';                    // new
import 'src/authentication.dart';                  // new
import 'src/widgets.dart';

import行では、Firebase CoreとAuthを導入し、ウィジェットツリーを介してアプリケーション状態オブジェクトを利用できるようにするために使用しているproviderパッケージをプルし、 lib/srcからの認証ウィジェットを含めます。

このアプリケーション状態オブジェクトであるApplicationStateには、このステップに対して2つの主要な責任がありますが、後のステップでアプリケーションに機能を追加すると、追加の責任が発生します。最初の責任は、 Firebase.initializeApp()を呼び出してFirebaseライブラリを初期化することです。その後、承認フローの処理が行われます。 lib/main.dartの最後に次のクラスを追加します。

lib / main.dart

class ApplicationState extends ChangeNotifier {
  ApplicationState() {
    init();
  }

  Future<void> init() async {
    await Firebase.initializeApp(
      options: DefaultFirebaseOptions.currentPlatform,
    );

    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loginState = ApplicationLoginState.loggedIn;
      } else {
        _loginState = ApplicationLoginState.loggedOut;
      }
      notifyListeners();
    });
  }

  ApplicationLoginState _loginState = ApplicationLoginState.loggedOut;
  ApplicationLoginState get loginState => _loginState;

  String? _email;
  String? get email => _email;

  void startLoginFlow() {
    _loginState = ApplicationLoginState.emailAddress;
    notifyListeners();
  }

  Future<void> verifyEmail(
    String email,
    void Function(FirebaseAuthException e) errorCallback,
  ) async {
    try {
      var methods =
          await FirebaseAuth.instance.fetchSignInMethodsForEmail(email);
      if (methods.contains('password')) {
        _loginState = ApplicationLoginState.password;
      } else {
        _loginState = ApplicationLoginState.register;
      }
      _email = email;
      notifyListeners();
    } on FirebaseAuthException catch (e) {
      errorCallback(e);
    }
  }

  Future<void> signInWithEmailAndPassword(
    String email,
    String password,
    void Function(FirebaseAuthException e) errorCallback,
  ) async {
    try {
      await FirebaseAuth.instance.signInWithEmailAndPassword(
        email: email,
        password: password,
      );
    } on FirebaseAuthException catch (e) {
      errorCallback(e);
    }
  }

  void cancelRegistration() {
    _loginState = ApplicationLoginState.emailAddress;
    notifyListeners();
  }

  Future<void> registerAccount(
      String email,
      String displayName,
      String password,
      void Function(FirebaseAuthException e) errorCallback) async {
    try {
      var credential = await FirebaseAuth.instance
          .createUserWithEmailAndPassword(email: email, password: password);
      await credential.user!.updateDisplayName(displayName);
    } on FirebaseAuthException catch (e) {
      errorCallback(e);
    }
  }

  void signOut() {
    FirebaseAuth.instance.signOut();
  }
}

このクラスのいくつかの重要なポイントに注意する価値があります。ユーザーは認証されていない状態で開始し、アプリはユーザーのメールアドレスを要求するフォームを表示します。そのメールアドレスが登録されているかどうかに応じて、アプリはユーザー登録を求めるか、パスワードを要求します。その後、すべてがうまくいくと仮定すると、ユーザーは認証されます。

これは、FirebaseUI Authフローの完全な実装ではないことに注意する必要があります。これは、ログインに問題がある既存のアカウントを持つユーザーのケースを処理しないためです。この追加機能の実装は、演習として残されています。やる気のある読者。

認証フローの統合

アプリケーションの状態が開始されたので、アプリケーションの状態をアプリの初期化に結び付け、認証フローをHomePageに追加します。メインエントリポイントを更新して、 providerパッケージを介してアプリケーションの状態を統合します。

lib / main.dart

void main() {
  // Modify from here
  runApp(
    ChangeNotifierProvider(
      create: (context) => ApplicationState(),
      builder: (context, _) => App(),
    ),
  );
  // to here.
}

main関数を変更すると、プロバイダーパッケージは、 ChangeNotifierProviderウィジェットを使用してアプリケーション状態オブジェクトをインスタンス化する必要があります。この特定のプロバイダークラスを使用しているのは、アプリケーション状態オブジェクトがChangeNotifierを拡張し、 providerパッケージが依存ウィジェットをいつ再表示するかを認識できるようにするためです。最後に、 HomePagebuildメソッドを更新して、アプリケーションの状態をAuthenticationと統合します。

lib / main.dart

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Firebase Meetup'),
      ),
      body: ListView(
        children: <Widget>[
          Image.asset('assets/codelab.png'),
          const SizedBox(height: 8),
          const IconAndDetail(Icons.calendar_today, 'October 30'),
          const IconAndDetail(Icons.location_city, 'San Francisco'),
          // Add from here
          Consumer<ApplicationState>(
            builder: (context, appState, _) => Authentication(
              email: appState.email,
              loginState: appState.loginState,
              startLoginFlow: appState.startLoginFlow,
              verifyEmail: appState.verifyEmail,
              signInWithEmailAndPassword: appState.signInWithEmailAndPassword,
              cancelRegistration: appState.cancelRegistration,
              registerAccount: appState.registerAccount,
              signOut: appState.signOut,
            ),
          ),
          // to here
          const Divider(
            height: 8,
            thickness: 1,
            indent: 8,
            endIndent: 8,
            color: Colors.grey,
          ),
          const Header("What we'll be doing"),
          const Paragraph(
            'Join us for a day full of Firebase Workshops and Pizza!',
          ),
        ],
      ),
    );
  }
}

Authenticationウィジェットをインスタンス化し、 Consumerウィジェットでラップします。コンシューマウィジェットは、アプリケーションの状態が変化したときに、 providerパッケージを使用してツリーの一部を再構築するための通常の方法です。 Authenticationウィジェットは、これからテストする認証UIです。

認証フローのテスト

cdf2d25e436bd48d.png

これが認証フローの開始です。ここで、ユーザーはRSVPボタンをタップして、電子メールフォームを開始できます。

2a2cd6d69d172369.png

電子メールを入力すると、システムはユーザーがすでに登録されているかどうかを確認します。登録されている場合はパスワードの入力を求められます。登録されていない場合は、登録フォームを使用します。

e5e65065dba36b54.png

エラー処理フローを確認するには、必ず短いパスワード(6文字未満)を入力してみてください。ユーザーが登録されている場合は、代わりにのパスワードが表示されます。

fbb3ea35fb4f67a.png

このページでは、このページのエラー処理を確認するために、間違ったパスワードを入力してください。最後に、ユーザーがログインすると、ユーザーに再度ログアウトする機能を提供するログインエクスペリエンスが表示されます。

4ed811a25b0cf816.png

これで、認証フローが実装されました。おめでとうございます!

6.CloudFirestoreにメッセージを書き込みます

ユーザーが来ていることを知っているのは素晴らしいことですが、ゲストにアプリで何か他のことをしてもらいましょう。ゲストブックにメッセージを残すことができたらどうでしょうか。彼らは、なぜ彼らが来ることに興奮しているのか、誰に会いたいのかを共有することができます。

ユーザーがアプリに書き込んだチャットメッセージを保存するには、 CloudFirestoreを使用します。

データ・モデル

Cloud FirestoreはNoSQLデータベースであり、データベースに格納されているデータは、コレクション、ドキュメント、フィールド、およびサブコレクションに分割されます。チャットの各メッセージは、 guestbookと呼ばれるトップレベルのコレクションにドキュメントとして保存されます。

7c20dc8424bb1d84.png

Firestoreにメッセージを追加する

このセクションでは、ユーザーがデータベースに新しいメッセージを書き込むための機能を追加します。まず、UI要素(フォームフィールドと送信ボタン)を追加し、次にこれらの要素をデータベースに接続するコードを追加します。

まず、 cloud_firestoreパッケージとdart:asyncのインポートを追加します。

lib / main.dart

import 'dart:async';                                    // new

import 'package:cloud_firestore/cloud_firestore.dart';  // new
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';

import 'firebase_options.dart';
import 'src/authentication.dart';
import 'src/widgets.dart';

メッセージフィールドと送信ボタンのUI要素を作成するには、 lib/main.dartの下部に新しいステートフルウィジェットGuestBookを追加します。

lib / main.dart

class GuestBook extends StatefulWidget {
  const GuestBook({required this.addMessage});
  final FutureOr<void> Function(String message) addMessage;

  @override
  _GuestBookState createState() => _GuestBookState();
}

class _GuestBookState extends State<GuestBook> {
  final _formKey = GlobalKey<FormState>(debugLabel: '_GuestBookState');
  final _controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Form(
        key: _formKey,
        child: Row(
          children: [
            Expanded(
              child: TextFormField(
                controller: _controller,
                decoration: const InputDecoration(
                  hintText: 'Leave a message',
                ),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Enter your message to continue';
                  }
                  return null;
                },
              ),
            ),
            const SizedBox(width: 8),
            StyledButton(
              onPressed: () async {
                if (_formKey.currentState!.validate()) {
                  await widget.addMessage(_controller.text);
                  _controller.clear();
                }
              },
              child: Row(
                children: const [
                  Icon(Icons.send),
                  SizedBox(width: 4),
                  Text('SEND'),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

ここにはいくつかの興味深い点があります。まず、フォームをインスタンス化して、メッセージに実際にコンテンツが含まれていることを検証し、コンテンツがない場合はユーザーにエラーメッセージを表示できるようにします。フォームを検証する方法には、フォームの背後にあるフォームの状態にアクセスすることが含まれます。このためには、 GlobalKeyを使用します。キーとその使用方法の詳細については、 FlutterWidgets101のエピソード「いつキーを使用するか」を参照してください。

また、ウィジェットのレイアウト方法に注意してください。 Rowがあり、 TextFormFieldStyledButtonがあり、それ自体にRowが含まれています。また、 TextFormFieldExpandedウィジェットでラップされていることに注意してください。これにより、 TextFormFieldは行内の余分なスペースを占有します。これが必要な理由をよりよく理解するには、 「制約の理解」をお読みください。

これで、ユーザーがテキストを入力してゲストブックに追加できるウィジェットができたので、それを画面に表示する必要があります。これを行うには、 HomePageの本文を編集して、 ListViewの子の下部に次の2行を追加します。

const Header("What we'll be doing"),
const Paragraph(
  'Join us for a day full of Firebase Workshops and Pizza!',
),
// Add the following two lines.
const Header('Discussion'),
GuestBook(addMessage: (message) => print(message)),

これはウィジェットを表示するのに十分ですが、何か便利なことをするのに十分ではありません。このコードをまもなく更新して、機能させる予定です。

アプリのプレビュー

ユーザーが[送信]ボタンをクリックすると、以下のコードスニペットがトリガーされます。メッセージ入力フィールドの内容をデータベースのguestbookコレクションに追加します。具体的には、 addMessageToGuestBookメソッドは、メッセージコンテンツを新しいドキュメント(自動生成されたIDを使用)のguestbookコレクションに追加します。

FirebaseAuth.instance.currentUser.uidがログインしているすべてのユーザーに提供する自動生成された一意のIDへの参照であることに注意してください。

lib/main.dartファイルに別の変更を加えます。 addMessageToGuestBookメソッドを追加します。次のステップでは、ユーザーインターフェイスとこの機能を一緒に配線します。

lib / main.dart

class ApplicationState extends ChangeNotifier {

  // Current content of ApplicationState elided ...

  // Add from here
  Future<DocumentReference> addMessageToGuestBook(String message) {
    if (_loginState != ApplicationLoginState.loggedIn) {
      throw Exception('Must be logged in');
    }

    return FirebaseFirestore.instance
        .collection('guestbook')
        .add(<String, dynamic>{
      'text': message,
      'timestamp': DateTime.now().millisecondsSinceEpoch,
      'name': FirebaseAuth.instance.currentUser!.displayName,
      'userId': FirebaseAuth.instance.currentUser!.uid,
    });
  }
  // To here
}

UIをデータベースに配線する

ユーザーがゲストブックに追加するテキストを入力できるUIがあり、CloudFirestoreにエントリを追加するためのコードがあります。今、あなたがする必要があるのは、2つを一緒に配線することです。 lib/main.dartで、 HomePageウィジェットに次の変更を加えます。

lib / main.dart

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Firebase Meetup'),
      ),
      body: ListView(
        children: <Widget>[
          Image.asset('assets/codelab.png'),
          const SizedBox(height: 8),
          const IconAndDetail(Icons.calendar_today, 'October 30'),
          const IconAndDetail(Icons.location_city, 'San Francisco'),
          Consumer<ApplicationState>(
            builder: (context, appState, _) => Authentication(
              email: appState.email,
              loginState: appState.loginState,
              startLoginFlow: appState.startLoginFlow,
              verifyEmail: appState.verifyEmail,
              signInWithEmailAndPassword: appState.signInWithEmailAndPassword,
              cancelRegistration: appState.cancelRegistration,
              registerAccount: appState.registerAccount,
              signOut: appState.signOut,
            ),
          ),
          const Divider(
            height: 8,
            thickness: 1,
            indent: 8,
            endIndent: 8,
            color: Colors.grey,
          ),
          const Header("What we'll be doing"),
          const Paragraph(
            'Join us for a day full of Firebase Workshops and Pizza!',
          ),
          // Modify from here
          Consumer<ApplicationState>(
            builder: (context, appState, _) => Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                if (appState.loginState == ApplicationLoginState.loggedIn) ...[
                  const Header('Discussion'),
                  GuestBook(
                    addMessage: (message) =>
                        appState.addMessageToGuestBook(message),
                  ),
                ],
              ],
            ),
          ),
          // To here.
        ],
      ),
    );
  }
}

このステップの開始時に追加した2行を、完全な実装に置き換えました。ここでも、 Consumer<ApplicationState>を使用して、レンダリングしているツリーの一部でアプリケーションの状態を利用できるようにしています。これにより、UIにメッセージを入力した人に反応し、それをデータベースに公開できます。次のセクションでは、追加されたメッセージがデータベースに公開されているかどうかをテストします。

メッセージの送信をテストする

  1. アプリにサインインしていることを確認してください。
  2. 「Heythere!」などのメッセージを入力し、[送信]をクリックします。

このアクションにより、メッセージがCloudFirestoreデータベースに書き込まれます。ただし、データの取得を実装する必要があるため、実際のFlutterアプリにはまだメッセージが表示されません。次のステップでそれを行います。

ただし、Firebaseコンソールで新しく追加されたメッセージを確認できます。

Firebaseコンソールのデータベースダッシュボードに、新しく追加されたメッセージを含むguestbookコレクションが表示されます。メッセージを送信し続けると、ゲストブックコレクションには次のような多くのドキュメントが含まれます。

Firebaseコンソール

713870af0b3b63c.png

7.メッセージを読む

ゲストがデータベースにメッセージを書き込むことができるのは素晴らしいことですが、アプリではまだメッセージを見ることができません。それを直そう!

メッセージを同期する

メッセージを表示するには、データが変更されたときにトリガーするリスナーを追加してから、新しいメッセージを表示するUI要素を作成する必要があります。アプリから新しく追加されたメッセージをリッスンするコードをアプリケーションの状態に追加します。

GuestBookウィジェットのすぐ上に次の値クラスがあります。このクラスは、CloudFirestoreに保存しているデータの構造化されたビューを公開します。

lib / main.dart

class GuestBookMessage {
  GuestBookMessage({required this.name, required this.message});
  final String name;
  final String message;
}

状態とゲッターを定義するApplicationStateのセクションに、次の新しい行を追加します。

lib / main.dart

  ApplicationLoginState _loginState = ApplicationLoginState.loggedOut;
  ApplicationLoginState get loginState => _loginState;

  String? _email;
  String? get email => _email;

  // Add from here
  StreamSubscription<QuerySnapshot>? _guestBookSubscription;
  List<GuestBookMessage> _guestBookMessages = [];
  List<GuestBookMessage> get guestBookMessages => _guestBookMessages;
  // to here.

最後に、 ApplicationStateの初期化セクションに次を追加して、ユーザーがログインしたときにドキュメントコレクションのクエリをサブスクライブし、ユーザーがログアウトしたときにサブスクライブを解除します。

lib / main.dart

  Future<void> init() async {
    await Firebase.initializeApp(
      options: DefaultFirebaseOptions.currentPlatform,
    );

    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loginState = ApplicationLoginState.loggedIn;
        // Add from here
        _guestBookSubscription = FirebaseFirestore.instance
            .collection('guestbook')
            .orderBy('timestamp', descending: true)
            .snapshots()
            .listen((snapshot) {
          _guestBookMessages = [];
          for (final document in snapshot.docs) {
            _guestBookMessages.add(
              GuestBookMessage(
                name: document.data()['name'] as String,
                message: document.data()['text'] as String,
              ),
            );
          }
          notifyListeners();
        });
        // to here.
      } else {
        _loginState = ApplicationLoginState.loggedOut;
        // Add from here
        _guestBookMessages = [];
        _guestBookSubscription?.cancel();
        // to here.
      }
      notifyListeners();
    });
  }

このセクションは重要です。ここでは、 guestbookコレクションに対してクエリを作成し、このコレクションのサブスクライブとサブスクライブ解除を処理します。ストリームをリッスンします。ここで、 guestbookコレクション内のメッセージのローカルキャッシュを再構築し、このサブスクリプションへの参照を保存して、後でサブスクリプションを解除できるようにします。ここでは多くのことが行われているので、より明確なメンタルモデルを取得するときに何が起こるかを調べるためにデバッガーで時間を費やす価値があります。

詳細については、 CloudFirestoreのドキュメントを参照してください。

GuestBookウィジェットでは、この変化する状態をユーザーインターフェイスに接続する必要があります。ウィジェットの構成の一部としてメッセージのリストを追加することにより、ウィジェットを変更します。

lib / main.dart

class GuestBook extends StatefulWidget {
  // Modify the following line
  const GuestBook({required this.addMessage, required this.messages});
  final FutureOr<void> Function(String message) addMessage;
  final List<GuestBookMessage> messages; // new

  @override
  _GuestBookState createState() => _GuestBookState();
}

次に、 buildメソッドを次のように変更して、この新しい構成を_GuestBookStateで公開します。

lib / main.dart

class _GuestBookState extends State<GuestBook> {
  final _formKey = GlobalKey<FormState>(debugLabel: '_GuestBookState');
  final _controller = TextEditingController();

  @override
  // Modify from here
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // to here.
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: Form(
            key: _formKey,
            child: Row(
              children: [
                Expanded(
                  child: TextFormField(
                    controller: _controller,
                    decoration: const InputDecoration(
                      hintText: 'Leave a message',
                    ),
                    validator: (value) {
                      if (value == null || value.isEmpty) {
                        return 'Enter your message to continue';
                      }
                      return null;
                    },
                  ),
                ),
                const SizedBox(width: 8),
                StyledButton(
                  onPressed: () async {
                    if (_formKey.currentState!.validate()) {
                      await widget.addMessage(_controller.text);
                      _controller.clear();
                    }
                  },
                  child: Row(
                    children: const [
                      Icon(Icons.send),
                      SizedBox(width: 4),
                      Text('SEND'),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
        // Modify from here
        const SizedBox(height: 8),
        for (var message in widget.messages)
          Paragraph('${message.name}: ${message.message}'),
        const SizedBox(height: 8),
      ],
      // to here.
    );
  }
}

buildメソッドの前のコンテンツをColumnウィジェットでラップしてから、 Columnの子の末尾にコレクションを追加して、メッセージのリスト内の各メッセージの新しいParagraphを生成します。

最後に、新しいmessagesパラメータを使用してGuestBookを正しく構築するために、 HomePageの本文を更新する必要があります。

lib / main.dart

Consumer<ApplicationState>(
  builder: (context, appState, _) => Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      if (appState.loginState == ApplicationLoginState.loggedIn) ...[
        const Header('Discussion'),
        GuestBook(
          addMessage: (message) =>
              appState.addMessageToGuestBook(message),
          messages: appState.guestBookMessages, // new
        ),
      ],
    ],
  ),
),

同期メッセージをテストする

Cloud Firestoreは、データベースにサブスクライブしているクライアントとデータを自動的かつ瞬時に同期します。

  1. データベースで以前に作成したメッセージは、アプリに表示されます。新しいメッセージを自由に書いてください。それらはすぐに表示されるはずです。
  2. ワークスペースを複数のウィンドウまたはタブで開くと、メッセージはタブ間でリアルタイムに同期されます。
  3. (オプション) Firebaseコンソールの[データベース]セクションで、新しいメッセージを手動で直接削除、変更、または追加してみることができます。変更はUIに表示されます。

おめでとう!アプリでCloudFirestoreドキュメントを読んでいます!

アプリpレビュー

8.基本的なセキュリティルールを設定します

最初に、テストモードを使用するようにCloud Firestoreを設定しました。これは、データベースが読み取りと書き込みのために開いていることを意味します。ただし、テストモードは、開発のごく初期の段階でのみ使用する必要があります。ベストプラクティスとして、アプリを開発するときにデータベースのセキュリティルールを設定する必要があります。セキュリティは、アプリの構造と動作に不可欠である必要があります。

セキュリティルールを使用すると、データベース内のドキュメントとコレクションへのアクセスを制御できます。柔軟なルール構文を使用すると、データベース全体へのすべての書き込みから特定のドキュメントの操作まで、あらゆるものに一致するルールを作成できます。

CloudFirestoreのセキュリティルールはFirebaseコンソールで作成できます。

  1. Firebaseコンソールの[開発]セクションで、[データベース]をクリックし、[ルール]タブを選択します(または、ここをクリックして[ルール]タブに直接移動します)。
  2. 次のデフォルトのセキュリティルールと、公開されているルールに関する警告が表示されます。

7767a2d2e64e7275.png

コレクションを特定する

まず、アプリがデータを書き込むコレクションを特定します。

match /databases/{database}/documentsで、保護するコレクションを特定します。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /guestbook/{entry} {
     // You'll add rules here in the next step.
  }
}

セキュリティルールを追加する

各ゲストブックドキュメントのフィールドとして認証UIDを使用したため、認証UIDを取得して、ドキュメントに書き込もうとしているユーザーが一致する認証UIDを持っていることを確認できます。

以下に示すように、読み取りルールと書き込みルールをルールセットに追加します。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /guestbook/{entry} {
      allow read: if request.auth.uid != null;
      allow write:
        if request.auth.uid == request.resource.data.userId;
    }
  }
}

これで、ゲストブックの場合、サインインしたユーザーのみがメッセージを読むことができます(任意のメッセージ!)が、メッセージの作成者のみがメッセージを編集できます。

検証ルールを追加する

データ検証を追加して、予想されるすべてのフィールドがドキュメントに存在することを確認します。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /guestbook/{entry} {
      allow read: if request.auth.uid != null;
      allow write:
      if request.auth.uid == request.resource.data.userId
          && "name" in request.resource.data
          && "text" in request.resource.data
          && "timestamp" in request.resource.data;
    }
  }
}

9.ボーナスステップ:学んだことを実践する

出席者の出欠確認ステータスを記録する

現在、このアプリでは、イベントに興味がある場合にチャットを開始できます。また、誰かが来ているかどうかを知る唯一の方法は、チャットに投稿するかどうかです。整理して、何人の人が来るのかを知らせましょう。

アプリケーションの状態にいくつかの新機能を追加します。 1つ目は、ログインしているユーザーが参加しているかどうかを指定する機能です。 2番目の機能は、実際に参加している人数のカウンターです。

lib/main.dartで、アクセサーセクションに以下を追加して、UIコードがこの状態と対話できるようにします。

lib / main.dart

int _attendees = 0;
int get attendees => _attendees;

Attending _attending = Attending.unknown;
StreamSubscription<DocumentSnapshot>? _attendingSubscription;
Attending get attending => _attending;
set attending(Attending attending) {
  final userDoc = FirebaseFirestore.instance
      .collection('attendees')
      .doc(FirebaseAuth.instance.currentUser!.uid);
  if (attending == Attending.yes) {
    userDoc.set(<String, dynamic>{'attending': true});
  } else {
    userDoc.set(<String, dynamic>{'attending': false});
  }
}

ApplicationStateinitメソッドを次のように更新します。

lib / main.dart

  Future<void> init() async {
    await Firebase.initializeApp(
      options: DefaultFirebaseOptions.currentPlatform,
    );

    // Add from here
    FirebaseFirestore.instance
        .collection('attendees')
        .where('attending', isEqualTo: true)
        .snapshots()
        .listen((snapshot) {
      _attendees = snapshot.docs.length;
      notifyListeners();
    });
    // To here

    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loginState = ApplicationLoginState.loggedIn;
        _guestBookSubscription = FirebaseFirestore.instance
            .collection('guestbook')
            .orderBy('timestamp', descending: true)
            .snapshots()
            .listen((snapshot) {
          _guestBookMessages = [];
          for (final document in snapshot.docs) {
            _guestBookMessages.add(
              GuestBookMessage(
                name: document.data()['name'] as String,
                message: document.data()['text'] as String,
              ),
            );
          }
          notifyListeners();
        });
        // Add from here
        _attendingSubscription = FirebaseFirestore.instance
            .collection('attendees')
            .doc(user.uid)
            .snapshots()
            .listen((snapshot) {
          if (snapshot.data() != null) {
            if (snapshot.data()!['attending'] as bool) {
              _attending = Attending.yes;
            } else {
              _attending = Attending.no;
            }
          } else {
            _attending = Attending.unknown;
          }
          notifyListeners();
        });
        // to here
      } else {
        _loginState = ApplicationLoginState.loggedOut;
        _guestBookMessages = [];
        _guestBookSubscription?.cancel();
        _attendingSubscription?.cancel(); // new
      }
      notifyListeners();
    });
  }

上記は、出席者の数を調べるために常にサブスクライブされたクエリと、ユーザーが出席しているかどうかを調べるためにユーザーがログインしている間だけアクティブになる2番目のクエリを追加します。次に、 GuestBookMessage宣言の後に次の列挙を追加します。

lib / main.dart

enum Attending { yes, no, unknown }

次に、古いラジオボタンのように機能する新しいウィジェットを定義します。はいもいいえも選択されていない不確定な状態で始まりますが、ユーザーが出席するかどうかを選択すると、そのオプションが塗りつぶされたボタンで強調表示され、他のオプションがフラットレンダリングで後退します。

lib / main.dart

class YesNoSelection extends StatelessWidget {
  const YesNoSelection({required this.state, required this.onSelection});
  final Attending state;
  final void Function(Attending selection) onSelection;

  @override
  Widget build(BuildContext context) {
    switch (state) {
      case Attending.yes:
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            children: [
              ElevatedButton(
                style: ElevatedButton.styleFrom(elevation: 0),
                onPressed: () => onSelection(Attending.yes),
                child: const Text('YES'),
              ),
              const SizedBox(width: 8),
              TextButton(
                onPressed: () => onSelection(Attending.no),
                child: const Text('NO'),
              ),
            ],
          ),
        );
      case Attending.no:
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            children: [
              TextButton(
                onPressed: () => onSelection(Attending.yes),
                child: const Text('YES'),
              ),
              const SizedBox(width: 8),
              ElevatedButton(
                style: ElevatedButton.styleFrom(elevation: 0),
                onPressed: () => onSelection(Attending.no),
                child: const Text('NO'),
              ),
            ],
          ),
        );
      default:
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            children: [
              StyledButton(
                onPressed: () => onSelection(Attending.yes),
                child: const Text('YES'),
              ),
              const SizedBox(width: 8),
              StyledButton(
                onPressed: () => onSelection(Attending.no),
                child: const Text('NO'),
              ),
            ],
          ),
        );
    }
  }
}

次に、 YesNoSelectionを利用するようにHomePageのビルドメソッドを更新して、ログインしているユーザーが参加しているかどうかを指定できるようにする必要があります。このイベントの参加者数も表示されます。

lib / main.dart

Consumer<ApplicationState>(
  builder: (context, appState, _) => Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      // Add from here
      if (appState.attendees >= 2)
        Paragraph('${appState.attendees} people going')
      else if (appState.attendees == 1)
        const Paragraph('1 person going')
      else
        const Paragraph('No one going'),
      // To here.
      if (appState.loginState == ApplicationLoginState.loggedIn) ...[
        // Add from here
        YesNoSelection(
          state: appState.attending,
          onSelection: (attending) => appState.attending = attending,
        ),
        // To here.
        const Header('Discussion'),
        GuestBook(
          addMessage: (message) =>
              appState.addMessageToGuestBook(message),
          messages: appState.guestBookMessages,
        ),
      ],
    ],
  ),
),

ルールを追加する

すでにいくつかのルールが設定されているため、ボタンを使用して追加する新しいデータは拒否されます。 attendeesコレクションに追加できるようにルールを更新する必要があります。

attendeesコレクションの場合、ドキュメント名として認証UIDを使用したため、それを取得して、送信者のuidが作成中のドキュメントと同じであることを確認できます。参加者リストは誰でも読むことができますが(プライベートデータがないため)、更新できるのは作成者だけです。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // ... //
    match /attendees/{userId} {
      allow read: if true;
      allow write: if request.auth.uid == userId;
    }
  }
}

検証ルールを追加する

データ検証を追加して、予想されるすべてのフィールドがドキュメントに存在することを確認します。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // ... //
    match /attendees/{userId} {
      allow read: if true;
      allow write: if request.auth.uid == userId
          && "attending" in request.resource.data;

    }
  }
}

(オプション)ボタンをクリックした結果を表示できるようになりました。 FirebaseコンソールでCloudFirestoreダッシュボードに移動します。

アプリのプレビュー

10.おめでとうございます!

Firebaseを使用して、インタラクティブなリアルタイムWebアプリケーションを構築しました。

私たちがカバーしたこと

  • Firebase認証
  • CloudFirestore
  • Firebaseのセキュリティルール

次のステップ

  • 他のFirebase製品についてもっと知りたいですか?ユーザーがアップロードした画像ファイルを保存したいですか?または、ユーザーに通知を送信しますか? Firebaseのドキュメントをご覧ください。 Firebase用のFlutterプラグインについて詳しく知りたいですか?詳細については、 FlutterFireを確認してください。
  • Cloud Firestoreについてもっと知りたいですか?サブコレクションとトランザクションについて知りたいですか? Cloud Firestoreの詳細については、CloudFirestoreWebコードラボにアクセスしてください。または、このYouTubeシリーズをチェックして、CloudFirestoreについて理解してください。

もっと詳しく知る

どうだった?

フィードバックをお待ちしております。こちらの(非常に)短いフォームに記入してください。

1.始める前に

このコードラボでは、AndroidとiOS向けのFlutterモバイルアプリを作成するためのFirebaseの基本のいくつかを学びます。

前提条件

このコードラボは、 Flutterに精通しており、FlutterSDKとエディターがインストールされていることを前提としています。

作成するもの

このコードラボでは、Flutterを使用して、Android、iOS、Web、およびmacOSでイベントRSVPおよびゲストブックチャットアプリを構築します。 Firebase認証でユーザーを認証し、CloudFirestoreを使用してデータを同期します。

必要なもの

このコードラボは、次のデバイスのいずれかを使用して実行できます。

  • コンピューターに接続され、開発者モードに設定されている物理デバイス(AndroidまたはiOS)。
  • iOSシミュレーター。 ( Xcodeツールをインストールする必要があります。)
  • Androidエミュレーター。 ( Android Studioでのセットアップが必要です。)

上記に加えて、次のものも必要になります。

  • Chromeなどの選択したブラウザ。
  • DartおよびFlutterプラグインで構成されたAndroidStudioVSCodeなど、選択したIDEまたはテキストエディター。
  • Flutterの最新のstableバージョン(または、エッジでの生活を楽しんでいる場合はbeta版)。
  • Firebaseプロジェクトを作成および管理するためのGmailアカウントなどのGoogleアカウント。
  • Gmailアカウントにログインしたfirebaseコマンドラインツール
  • codelabのサンプルコード。コードを取得する方法については、次の手順を参照してください。

2.サンプルコードを取得します

GitHubからプロジェクトの初期バージョンをダウンロードすることから始めましょう。

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

git clone https://github.com/flutter/codelabs.git flutter-codelabs

または、 GitHubのCLIツールがインストールされている場合:

gh repo clone flutter/codelabs flutter-codelabs

サンプルコードは、codelabsのコレクションのコードが含まれているflutter-codelabsディレクトリに複製する必要があります。このコードラボのコードは、 flutter-codelabs/firebase-get-to-know-flutterます。

flutter-codelabs/firebase-get-to-know-flutter下のディレクトリ構造は、名前が付けられた各ステップの最後にあるべき場所の一連のスナップショットです。これはステップ2であるため、一致するファイルを見つけるのは次のように簡単です。

cd flutter-codelabs/firebase-get-to-know-flutter/step_02

先にスキップしたい場合、またはステップの後に何かがどのように見えるかを確認したい場合は、関心のあるステップにちなんで名付けられたディレクトリを調べてください。

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

flutter-codelabs/firebase-get-to-know-flutter/step_02ディレクトリを開くかインポートします。このディレクトリには、まだ機能していないFlutterミートアップアプリで構成されるcodelabの開始コードが含まれています。

作業するファイルを見つけます

このアプリのコードは複数のディレクトリに分散しています。この機能の分割は、コードを機能ごとにグループ化することにより、作業を容易にするように設計されています。

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

  • lib/main.dart :このファイルには、メインのエントリポイントとアプリケーションウィジェットが含まれています。
  • lib/src/widgets.dart :このファイルには、アプリケーションのスタイルを標準化するのに役立つウィジェットがいくつか含まれています。これらは、スターターアプリの画面を構成するために使用されます。
  • lib/src/authentication.dart :このファイルには、Firebaseメールベース認証のログインユーザーエクスペリエンスを作成するための一連のウィジェットを備えたFirebaseUIAuthの部分的な実装が含まれています。認証フロー用のこれらのウィジェットは、スターターアプリではまだ使用されていませんが、すぐに接続します。

アプリケーションの残りの部分を構築するために、必要に応じてファイルを追加します。

lib/main.dartファイルを確認する

このアプリはgoogle_fontsパッケージを利用して、アプリ全体でRobotoをデフォルトのフォントにすることができます。やる気のある読者のための演習は、 fonts.google.comを探索し、アプリのさまざまな部分で見つけたフォントを使用することです。

lib/src/widgets.dartのヘルパーウィジェットをHeaderParagraphIconAndDetailの形式で利用しています。これらのウィジェットは、重複するコードを排除することにより、 HomePageで説明されているページレイアウトの乱雑さを軽減します。これには、一貫したルックアンドフィールを可能にするという追加の利点があります。

Android、iOS、Web、macOSでのアプリの外観は次のとおりです。

アプリのプレビュー

3.Firebaseプロジェクトを作成して設定します

イベント情報を表示することはゲストにとっては素晴らしいことですが、イベントを表示するだけでは誰にとってもあまり役に立ちません。このアプリにいくつかの動的機能を追加しましょう。このためには、Firebaseをアプリに接続する必要があります。 Firebaseの使用を開始するには、Firebaseプロジェクトを作成して設定する必要があります。

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

  1. Firebaseにログインします。
  2. Firebaseコンソールで、[プロジェクトの追加](または[プロジェクトの作成])をクリックし、FirebaseプロジェクトにFirebase-Flutter-Codelabという名前を付けます。

4395e4e67c08043a.png

  1. プロジェクト作成オプションをクリックします。プロンプトが表示されたら、Firebaseの利用規約に同意します。 Skip setting up Google Analytics, because you won't be using Analytics for this app.

b7138cde5f2c7b61.png

To learn more about Firebase projects, see Understand Firebase projects .

The app that you're building uses several Firebase products that are available for web apps:

  • Firebase Authentication to allow your users to sign in to your app.
  • Cloud Firestore to save structured data on the cloud and get instant notification when data changes.
  • Firebase Security Rules to secure your database.

Some of these products need special configuration or need to be enabled using the Firebase console.

Enable email sign-in for Firebase Authentication

To allow users to sign in to the web app, you'll use the Email/Password sign-in method for this codelab:

  1. In the Firebase console, expand the Build menu in the left panel.
  2. Click Authentication , and then click the Get Started button, then the Sign-in method tab (or click here to go directly to the Sign-in method tab).
  3. Click Email/Password in the Sign-in providers list, set the Enable switch to the on position, and then click Save . 58e3e3e23c2f16a4.png

Enable Cloud Firestore

The web app uses Cloud Firestore to save chat messages and receive new chat messages.

Enable Cloud Firestore:

  1. In the Firebase console's Build section, click Cloud Firestore .
  2. Click Create database . 99e8429832d23fa3.png
  1. Select the Start in test mode option. Read the disclaimer about the security rules. Test mode ensures that you can freely write to the database during development. Click Next . 6be00e26c72ea032.png
  1. Select the location for your database (You can just use the default). Note that this location can't be changed later. 278656eefcfb0216.png
  2. Click Enable .

4. Firebase configuration

In order to use Firebase with Flutter, you need to follow a process to configure the Flutter project to utilise the FlutterFire libraries correctly:

  • Add the FlutterFire dependencies to your project
  • Register the desired platform on the Firebase project
  • Download the platform-specific configuration file, and add it to the code.

In the top-level directory of your Flutter app, there are subdirectories called android , ios , macos and web . These directories hold the platform-specific configuration files for iOS and Android, respectively.

Configure dependencies

You need to add the FlutterFire libraries for the two Firebase products you are utilizing in this app - Firebase Auth and Cloud Firestore. Run the following three commands to add the depencies.

$ flutter pub add firebase_core 
Resolving dependencies...
+ firebase_core 1.10.5
+ firebase_core_platform_interface 4.2.2
+ firebase_core_web 1.5.2
+ flutter_web_plugins 0.0.0 from sdk flutter
+ js 0.6.3
  test_api 0.4.3 (0.4.8 available)
Changed 5 dependencies!

The firebase_core is the common code required for all Firebase Flutter plugins.

$ flutter pub add firebase_auth
Resolving dependencies...
+ firebase_auth 3.3.3
+ firebase_auth_platform_interface 6.1.8
+ firebase_auth_web 3.3.4
+ intl 0.17.0
  test_api 0.4.3 (0.4.8 available)
Changed 4 dependencies!

The firebase_auth enables integration with Firebase's Authentication capability.

$ flutter pub add cloud_firestore
Resolving dependencies...
+ cloud_firestore 3.1.4
+ cloud_firestore_platform_interface 5.4.9
+ cloud_firestore_web 2.6.4
  test_api 0.4.3 (0.4.8 available)
Changed 3 dependencies!

The cloud_firestore enables access to Cloud Firestore data storage.

$ flutter pub add provider
Resolving dependencies...
+ nested 1.0.0
+ provider 6.0.1
  test_api 0.4.3 (0.4.8 available)
Changed 2 dependencies!

While you have added the required packages, you also need to configure the iOS, Android, macOS and Web runner projects to appropriately utilise Firebase. You are also using the provider package that will enable separation of business logic from display logic.

Installing flutterfire

The FlutterFire CLI depends on the underlying Firebase CLI. If you haven't done so already, ensure the Firebase CLI is installed on your machine.

Next, install the FlutterFire CLI by running the following command:

$ dart pub global activate flutterfire_cli

Once installed, the flutterfire command will be globally available.

Configuring your apps

The CLI extracts information from your Firebase project and selected project applications to generate all the configuration for a specific platform.

In the root of your application, run the configure command:

$ flutterfire configure

The configuration command will guide you through a number of processes:

  1. Selecting a Firebase project (based on the .firebaserc file or from the Firebase Console).
  2. Prompt what platforms (eg Android, iOS, macOS & web) you would like configuration for.
  3. Identify which Firebase applications for the chosen platforms should be used to extract configuration for. By default, the CLI will attempt to automatically match Firebase apps based on your current project configuration.
  4. Generate a firebase_options.dart file in your project.

Configure macOS

Flutter on macOS builds fully sandboxed applications. As this application is integrating using the network to communicate with the Firebase servers, you will need to configure your application with network client privileges.

macos/Runner/DebugProfile.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.app-sandbox</key>
	<true/>
	<key>com.apple.security.cs.allow-jit</key>
	<true/>
	<key>com.apple.security.network.server</key>
	<true/>
  <!-- Add the following two lines -->
	<key>com.apple.security.network.client</key>
	<true/>
</dict>
</plist>

macos/Runner/Release.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.app-sandbox</key>
	<true/>
  <!-- Add the following two lines -->
	<key>com.apple.security.network.client</key>
	<true/>
</dict>
</plist>

See Entitlements and the App Sandbox for more detail.

5. Add user sign-in (RSVP)

Now that you've added Firebase to the app, you can set up an RSVP button that registers people using Firebase Authentication . For Android native, iOS native, and Web there are pre-built FirebaseUI Auth packages, but for Flutter you will need to build this capability.

The project you retrieved in Step 2 included a set of widgets that implements the user interface for most of the authentication flow. You will implement the business logic to integrate Firebase Authentication into the application.

Business Logic with Provider

You are going to use the provider package to make a centralized application state object available throughout the application's tree of Flutter widgets. To start with, modify the imports at the top of lib/main.dart :

lib/main.dart

import 'package:firebase_auth/firebase_auth.dart'; // new
import 'package:firebase_core/firebase_core.dart'; // new
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';           // new

import 'firebase_options.dart';                    // new
import 'src/authentication.dart';                  // new
import 'src/widgets.dart';

The import lines introduce Firebase Core and Auth, pull in the provider package which you are using to make the application state object available through the widget tree, and include the authentication widgets from lib/src .

This application state object, ApplicationState , has two main responsibilities for this step, but will gain additional responsibilities as you add more capabilities to the application in later steps. The first responsibility is to initialize the Firebase library with a call to Firebase.initializeApp() , and then there is the handling of the authorization flow. Add the following class to the end of lib/main.dart :

lib/main.dart

class ApplicationState extends ChangeNotifier {
  ApplicationState() {
    init();
  }

  Future<void> init() async {
    await Firebase.initializeApp(
      options: DefaultFirebaseOptions.currentPlatform,
    );

    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loginState = ApplicationLoginState.loggedIn;
      } else {
        _loginState = ApplicationLoginState.loggedOut;
      }
      notifyListeners();
    });
  }

  ApplicationLoginState _loginState = ApplicationLoginState.loggedOut;
  ApplicationLoginState get loginState => _loginState;

  String? _email;
  String? get email => _email;

  void startLoginFlow() {
    _loginState = ApplicationLoginState.emailAddress;
    notifyListeners();
  }

  Future<void> verifyEmail(
    String email,
    void Function(FirebaseAuthException e) errorCallback,
  ) async {
    try {
      var methods =
          await FirebaseAuth.instance.fetchSignInMethodsForEmail(email);
      if (methods.contains('password')) {
        _loginState = ApplicationLoginState.password;
      } else {
        _loginState = ApplicationLoginState.register;
      }
      _email = email;
      notifyListeners();
    } on FirebaseAuthException catch (e) {
      errorCallback(e);
    }
  }

  Future<void> signInWithEmailAndPassword(
    String email,
    String password,
    void Function(FirebaseAuthException e) errorCallback,
  ) async {
    try {
      await FirebaseAuth.instance.signInWithEmailAndPassword(
        email: email,
        password: password,
      );
    } on FirebaseAuthException catch (e) {
      errorCallback(e);
    }
  }

  void cancelRegistration() {
    _loginState = ApplicationLoginState.emailAddress;
    notifyListeners();
  }

  Future<void> registerAccount(
      String email,
      String displayName,
      String password,
      void Function(FirebaseAuthException e) errorCallback) async {
    try {
      var credential = await FirebaseAuth.instance
          .createUserWithEmailAndPassword(email: email, password: password);
      await credential.user!.updateDisplayName(displayName);
    } on FirebaseAuthException catch (e) {
      errorCallback(e);
    }
  }

  void signOut() {
    FirebaseAuth.instance.signOut();
  }
}

It is worth noting a few key points in this class. The user starts off unauthenticated, the app shows a form requesting the user's email address, depending on whether that email address is on file, the app will either ask the user register, or request their password, and then assuming everything works out, the user is authenticated.

It must be noted that this isn't a complete implementation of the FirebaseUI Auth flow, as it does not handle the case of a user with an existing account who is having trouble logging in. Implementing this additional capability is left as an exercise to the motivated reader.

Integrating the Authentication flow

Now that you have the start of the application state it is time to wire the application state into the app initialization and add the authentication flow into HomePage . Update the main entry point to integrate application state via the provider package:

lib/main.dart

void main() {
  // Modify from here
  runApp(
    ChangeNotifierProvider(
      create: (context) => ApplicationState(),
      builder: (context, _) => App(),
    ),
  );
  // to here.
}

The modification to the main function makes the provider package responsible for instantiating the application state object using the ChangeNotifierProvider widget. You are using this specific provider class because the application state object extends ChangeNotifier and this enables the provider package to know when to redisplay dependent widgets. Finally, integrate the application state with Authentication by updating HomePage 's build method:

lib/main.dart

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Firebase Meetup'),
      ),
      body: ListView(
        children: <Widget>[
          Image.asset('assets/codelab.png'),
          const SizedBox(height: 8),
          const IconAndDetail(Icons.calendar_today, 'October 30'),
          const IconAndDetail(Icons.location_city, 'San Francisco'),
          // Add from here
          Consumer<ApplicationState>(
            builder: (context, appState, _) => Authentication(
              email: appState.email,
              loginState: appState.loginState,
              startLoginFlow: appState.startLoginFlow,
              verifyEmail: appState.verifyEmail,
              signInWithEmailAndPassword: appState.signInWithEmailAndPassword,
              cancelRegistration: appState.cancelRegistration,
              registerAccount: appState.registerAccount,
              signOut: appState.signOut,
            ),
          ),
          // to here
          const Divider(
            height: 8,
            thickness: 1,
            indent: 8,
            endIndent: 8,
            color: Colors.grey,
          ),
          const Header("What we'll be doing"),
          const Paragraph(
            'Join us for a day full of Firebase Workshops and Pizza!',
          ),
        ],
      ),
    );
  }
}

You instantiate the Authentication widget, and wrap it in a Consumer widget. The Consumer widget the usual way that the provider package can be used to rebuild part of the tree when the application state changes. The Authentication widget is the authentication UI that you will now test.

Testing the Authentication flow

cdf2d25e436bd48d.png

Here is the start of the authentication flow, where the user can tap on the RSVP button, to initiate the email form.

2a2cd6d69d172369.png

Upon entering the email, the system confirms if the user is already registered, in which case the user is prompted for a password, alternatively if the user isn't registered, then they go through the registration form.

e5e65065dba36b54.png

Make sure to try out entering a short password (less than six characters) to check the error handling flow. If the user is registered, they will see the password for instead.

fbb3ea35fb4f67a.png

On this page make sure to enter incorrect passwords to check the error handling on this page. Finally, once the user is logged in, you will see the logged in experience which offers the user the ability to log out again.

4ed811a25b0cf816.png

And with that, you have implemented an authentication flow. Congrats!

6. Write messages to Cloud Firestore

Knowing that users are coming is great, but let's give the guests something else to do in the app. What if they could leave messages in a guestbook? They can share why they're excited to come or who they hope to meet.

To store the chat messages that users write in the app, you'll use Cloud Firestore .

Data model

Cloud Firestore is a NoSQL database, and data stored in the database is split into collections, documents, fields, and subcollections. You will store each message of the chat as a document in a top-level collection called guestbook .

7c20dc8424bb1d84.png

Add messages to Firestore

In this section, you'll add the functionality for users to write new messages to the database. First, you add the UI elements (form field and send button), and then you add the code that hooks these elements up to the database.

First, add imports for the cloud_firestore package and dart:async .

lib/main.dart

import 'dart:async';                                    // new

import 'package:cloud_firestore/cloud_firestore.dart';  // new
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';

import 'firebase_options.dart';
import 'src/authentication.dart';
import 'src/widgets.dart';

To construct the UI elements of a message field and a send button, add a new stateful widget GuestBook at the bottom of lib/main.dart .

lib/main.dart

class GuestBook extends StatefulWidget {
  const GuestBook({required this.addMessage});
  final FutureOr<void> Function(String message) addMessage;

  @override
  _GuestBookState createState() => _GuestBookState();
}

class _GuestBookState extends State<GuestBook> {
  final _formKey = GlobalKey<FormState>(debugLabel: '_GuestBookState');
  final _controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Form(
        key: _formKey,
        child: Row(
          children: [
            Expanded(
              child: TextFormField(
                controller: _controller,
                decoration: const InputDecoration(
                  hintText: 'Leave a message',
                ),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Enter your message to continue';
                  }
                  return null;
                },
              ),
            ),
            const SizedBox(width: 8),
            StyledButton(
              onPressed: () async {
                if (_formKey.currentState!.validate()) {
                  await widget.addMessage(_controller.text);
                  _controller.clear();
                }
              },
              child: Row(
                children: const [
                  Icon(Icons.send),
                  SizedBox(width: 4),
                  Text('SEND'),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

There are a couple of points of interest here. First up, you are instantiating a Form so you can validate the message actually has some content, and show the user an error message if there isn't any. The way to validate a form involves accessing the form state behind the form, and for this you use a GlobalKey . For more information on Keys, and how to use them, please see the Flutter Widgets 101 episode "When to Use Keys" .

Also note the way the widgets are laid out, you have a Row , with a TextFormField and a StyledButton , which itself contains a Row . Also note the TextFormField is wrapped in an Expanded widget, this forces the TextFormField to take up any extra space in the row. To better understand why this is required, please read through Understanding constraints .

Now that you have a widget that enables the user to enter some text to add to the Guest Book, you need to get it on the screen. To do so, edit the body of HomePage to add the following two lines at the bottom of the ListView 's children:

const Header("What we'll be doing"),
const Paragraph(
  'Join us for a day full of Firebase Workshops and Pizza!',
),
// Add the following two lines.
const Header('Discussion'),
GuestBook(addMessage: (message) => print(message)),

While this is enough to display the Widget, it isn't sufficient to do anything useful. You will update this code shortly to make it functional.

App preview

A user clicking the SEND button will trigger the code snippet below. It adds the contents of the message input field to the guestbook collection of the database. Specifically, the addMessageToGuestBook method adds the message content to a new document (with an automatically generated ID) to the guestbook collection.

Note that FirebaseAuth.instance.currentUser.uid is a reference to the auto-generated unique ID that Firebase Authentication gives for all logged-in users.

Make another change to the lib/main.dart file. Add the addMessageToGuestBook method. You will wire the user interface and this capability together in the next step.

lib/main.dart

class ApplicationState extends ChangeNotifier {

  // Current content of ApplicationState elided ...

  // Add from here
  Future<DocumentReference> addMessageToGuestBook(String message) {
    if (_loginState != ApplicationLoginState.loggedIn) {
      throw Exception('Must be logged in');
    }

    return FirebaseFirestore.instance
        .collection('guestbook')
        .add(<String, dynamic>{
      'text': message,
      'timestamp': DateTime.now().millisecondsSinceEpoch,
      'name': FirebaseAuth.instance.currentUser!.displayName,
      'userId': FirebaseAuth.instance.currentUser!.uid,
    });
  }
  // To here
}

Wiring the UI into the database

You have a UI where the user can enter the text they want to add to the Guest Book, and you have the code to add the entry to Cloud Firestore. Now all you need to do is wire the two together. In lib/main.dart make the following change to the HomePage widget.

lib/main.dart

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Firebase Meetup'),
      ),
      body: ListView(
        children: <Widget>[
          Image.asset('assets/codelab.png'),
          const SizedBox(height: 8),
          const IconAndDetail(Icons.calendar_today, 'October 30'),
          const IconAndDetail(Icons.location_city, 'San Francisco'),
          Consumer<ApplicationState>(
            builder: (context, appState, _) => Authentication(
              email: appState.email,
              loginState: appState.loginState,
              startLoginFlow: appState.startLoginFlow,
              verifyEmail: appState.verifyEmail,
              signInWithEmailAndPassword: appState.signInWithEmailAndPassword,
              cancelRegistration: appState.cancelRegistration,
              registerAccount: appState.registerAccount,
              signOut: appState.signOut,
            ),
          ),
          const Divider(
            height: 8,
            thickness: 1,
            indent: 8,
            endIndent: 8,
            color: Colors.grey,
          ),
          const Header("What we'll be doing"),
          const Paragraph(
            'Join us for a day full of Firebase Workshops and Pizza!',
          ),
          // Modify from here
          Consumer<ApplicationState>(
            builder: (context, appState, _) => Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                if (appState.loginState == ApplicationLoginState.loggedIn) ...[
                  const Header('Discussion'),
                  GuestBook(
                    addMessage: (message) =>
                        appState.addMessageToGuestBook(message),
                  ),
                ],
              ],
            ),
          ),
          // To here.
        ],
      ),
    );
  }
}

You have replaced the two lines you added back at the start of this step with the full implementation. You are again using Consumer<ApplicationState> to make the application state available to the part of the tree you are rendering. This enables you to react to someone entering a message in the UI, and publish it into the database. In the next section you will test if the added messages are published into the database.

Test sending messages

  1. Make sure that you're signed in to the app.
  2. Enter a message such as "Hey there!", and then click SEND .

This action writes the message to your Cloud Firestore database. However, you won't yet see the message in your actual Flutter app because you still need to implement retrieving the data. You'll do that in the next step.

But you can see the newly added message in the Firebase console.

In the Firebase console, in the Database dashboard , you should see the guestbook collection with your newly added message. If you keep sending messages, your guestbook collection will contain many documents, like this:

Firebase console

713870af0b3b63c.png

7. Read messages

It's lovely that guests can write messages to the database, but they can't see them in the app yet. Let's fix that!

Synchronize messages

To display messages, you'll need to add listeners that trigger when data changes and then create a UI element that shows new messages. You'll add code to the application state that listens for newly added messages from the app.

Just above the GuestBook widget the following value class. This class exposes a structured view of the data you are storing in Cloud Firestore.

lib/main.dart

class GuestBookMessage {
  GuestBookMessage({required this.name, required this.message});
  final String name;
  final String message;
}

In the section of ApplicationState where you define state and getters, add the following new lines:

lib/main.dart

  ApplicationLoginState _loginState = ApplicationLoginState.loggedOut;
  ApplicationLoginState get loginState => _loginState;

  String? _email;
  String? get email => _email;

  // Add from here
  StreamSubscription<QuerySnapshot>? _guestBookSubscription;
  List<GuestBookMessage> _guestBookMessages = [];
  List<GuestBookMessage> get guestBookMessages => _guestBookMessages;
  // to here.

And finally, in the initialization section of ApplicationState , add the following to subscribe to a query over the document collection when a user logs in, and unsubscribe when they log out.

lib/main.dart

  Future<void> init() async {
    await Firebase.initializeApp(
      options: DefaultFirebaseOptions.currentPlatform,
    );

    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loginState = ApplicationLoginState.loggedIn;
        // Add from here
        _guestBookSubscription = FirebaseFirestore.instance
            .collection('guestbook')
            .orderBy('timestamp', descending: true)
            .snapshots()
            .listen((snapshot) {
          _guestBookMessages = [];
          for (final document in snapshot.docs) {
            _guestBookMessages.add(
              GuestBookMessage(
                name: document.data()['name'] as String,
                message: document.data()['text'] as String,
              ),
            );
          }
          notifyListeners();
        });
        // to here.
      } else {
        _loginState = ApplicationLoginState.loggedOut;
        // Add from here
        _guestBookMessages = [];
        _guestBookSubscription?.cancel();
        // to here.
      }
      notifyListeners();
    });
  }

This section is important, as here is where you construct a query over the guestbook collection, and handle subscribing and unsubscribing to this collection. You listen to the stream, where you reconstruct a local cache of the messages in the guestbook collection, and also store a reference to this subscription so you can unsubscribe from it later. There is a lot going on here, and it is worth spending some time in a debugger inspecting what happens when to get a clearer mental model.

For more information, see the Cloud Firestore documentation .

In the GuestBook widget you need to connect this changing state to the user interface. You modify the widget by adding a list of messages as part of its configuration.

lib/main.dart

class GuestBook extends StatefulWidget {
  // Modify the following line
  const GuestBook({required this.addMessage, required this.messages});
  final FutureOr<void> Function(String message) addMessage;
  final List<GuestBookMessage> messages; // new

  @override
  _GuestBookState createState() => _GuestBookState();
}

Next, we expose this new configuration in _GuestBookState by modifying the build method as follows.

lib/main.dart

class _GuestBookState extends State<GuestBook> {
  final _formKey = GlobalKey<FormState>(debugLabel: '_GuestBookState');
  final _controller = TextEditingController();

  @override
  // Modify from here
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // to here.
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: Form(
            key: _formKey,
            child: Row(
              children: [
                Expanded(
                  child: TextFormField(
                    controller: _controller,
                    decoration: const InputDecoration(
                      hintText: 'Leave a message',
                    ),
                    validator: (value) {
                      if (value == null || value.isEmpty) {
                        return 'Enter your message to continue';
                      }
                      return null;
                    },
                  ),
                ),
                const SizedBox(width: 8),
                StyledButton(
                  onPressed: () async {
                    if (_formKey.currentState!.validate()) {
                      await widget.addMessage(_controller.text);
                      _controller.clear();
                    }
                  },
                  child: Row(
                    children: const [
                      Icon(Icons.send),
                      SizedBox(width: 4),
                      Text('SEND'),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
        // Modify from here
        const SizedBox(height: 8),
        for (var message in widget.messages)
          Paragraph('${message.name}: ${message.message}'),
        const SizedBox(height: 8),
      ],
      // to here.
    );
  }
}

You wrap the previous content of the build method with a Column widget, and then at the tail of the Column 's children you add a collection for to generate a new Paragraph for each message in the list of messages.

Finally, you now need to update the body of HomePage to correctly construct GuestBook with the new messages parameter.

lib/main.dart

Consumer<ApplicationState>(
  builder: (context, appState, _) => Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      if (appState.loginState == ApplicationLoginState.loggedIn) ...[
        const Header('Discussion'),
        GuestBook(
          addMessage: (message) =>
              appState.addMessageToGuestBook(message),
          messages: appState.guestBookMessages, // new
        ),
      ],
    ],
  ),
),

Test synchronizing messages

Cloud Firestore automatically and instantly synchronizes data with clients subscribed to the database.

  1. The messages that you created earlier in the database should be displayed in the app. Feel free to write new messages; they should appear instantly.
  2. If you open your workspace in multiple windows or tabs, messages will sync in real time across tabs.
  3. (Optional) You can try manually deleting, modifying, or adding new messages directly in the Database section of the Firebase console; any changes should appear in the UI.

Congratulations! You are reading Cloud Firestore documents in your app!

App p review

8. Set up basic security rules

You initially set up Cloud Firestore to use test mode, meaning that your database is open for reads and writes. However, you should only use test mode during very early stages of development. As a best practice, you should set up security rules for your database as you develop your app. Security should be integral to your app's structure and behavior.

Security Rules allow you to control access to documents and collections in your database. The flexible rules syntax allows you to create rules that match anything from all writes to the entire database to operations on a specific document.

You can write security rules for Cloud Firestore in the Firebase console:

  1. In the Firebase console's Develop section, click Database , and then select the Rules tab (or click here to go directly to the Rules tab).
  2. You should see the following default security rules, along with a warning about the rules being public.

7767a2d2e64e7275.png

Identify collections

First, identify the collections to which the app writes data.

In match /databases/{database}/documents , identify the collection that you want to secure:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /guestbook/{entry} {
     // You'll add rules here in the next step.
  }
}

Add security rules

Because you used the Authentication UID as a field in each guestbook document, you can get the Authentication UID and verify that anyone attempting to write to the document has a matching Authentication UID.

Add the read and write rules to your rule set as shown below:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /guestbook/{entry} {
      allow read: if request.auth.uid != null;
      allow write:
        if request.auth.uid == request.resource.data.userId;
    }
  }
}

Now, for the guestbook, only signed-in users can read messages (any message!), but only a message's author can edit a message.

Add validation rules

Add data validation to make sure that all of the expected fields are present in the document:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /guestbook/{entry} {
      allow read: if request.auth.uid != null;
      allow write:
      if request.auth.uid == request.resource.data.userId
          && "name" in request.resource.data
          && "text" in request.resource.data
          && "timestamp" in request.resource.data;
    }
  }
}

9. Bonus step: Practice what you've learned

Record an attendee's RSVP status

Right now, your app just allows people to start chatting if they're interested in the event. Also, the only way you know if someone's coming is if they post it in the chat. Let's get organized and let people know how many people are coming.

You are going to add a couple of new capabilities to the application state. The first is the ability for a logged in user to nominate if they are attending or not. The second capability is a counter of how many people are actually attending.

In lib/main.dart , add the following to the accessors section to enable the UI code to interact with this state:

lib/main.dart

int _attendees = 0;
int get attendees => _attendees;

Attending _attending = Attending.unknown;
StreamSubscription<DocumentSnapshot>? _attendingSubscription;
Attending get attending => _attending;
set attending(Attending attending) {
  final userDoc = FirebaseFirestore.instance
      .collection('attendees')
      .doc(FirebaseAuth.instance.currentUser!.uid);
  if (attending == Attending.yes) {
    userDoc.set(<String, dynamic>{'attending': true});
  } else {
    userDoc.set(<String, dynamic>{'attending': false});
  }
}

Update ApplicationState 's init method as follows:

lib/main.dart

  Future<void> init() async {
    await Firebase.initializeApp(
      options: DefaultFirebaseOptions.currentPlatform,
    );

    // Add from here
    FirebaseFirestore.instance
        .collection('attendees')
        .where('attending', isEqualTo: true)
        .snapshots()
        .listen((snapshot) {
      _attendees = snapshot.docs.length;
      notifyListeners();
    });
    // To here

    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loginState = ApplicationLoginState.loggedIn;
        _guestBookSubscription = FirebaseFirestore.instance
            .collection('guestbook')
            .orderBy('timestamp', descending: true)
            .snapshots()
            .listen((snapshot) {
          _guestBookMessages = [];
          for (final document in snapshot.docs) {
            _guestBookMessages.add(
              GuestBookMessage(
                name: document.data()['name'] as String,
                message: document.data()['text'] as String,
              ),
            );
          }
          notifyListeners();
        });
        // Add from here
        _attendingSubscription = FirebaseFirestore.instance
            .collection('attendees')
            .doc(user.uid)
            .snapshots()
            .listen((snapshot) {
          if (snapshot.data() != null) {
            if (snapshot.data()!['attending'] as bool) {
              _attending = Attending.yes;
            } else {
              _attending = Attending.no;
            }
          } else {
            _attending = Attending.unknown;
          }
          notifyListeners();
        });
        // to here
      } else {
        _loginState = ApplicationLoginState.loggedOut;
        _guestBookMessages = [];
        _guestBookSubscription?.cancel();
        _attendingSubscription?.cancel(); // new
      }
      notifyListeners();
    });
  }

The above adds an always subscribed query to find out the number of attendees, and a second query that is only active while a user is logged in to find out if the user is attending. Next, add the following enumeration after the GuestBookMessage declaration:

lib/main.dart

enum Attending { yes, no, unknown }

You are now going to define a new widget that acts like radio buttons of old. It starts off in an indeterminate state, with neither yes nor no selected, but once the user selects whether they are attending or not, then you show that option highlighted with a filled button, and the other option receding with a flat rendering.

lib/main.dart

class YesNoSelection extends StatelessWidget {
  const YesNoSelection({required this.state, required this.onSelection});
  final Attending state;
  final void Function(Attending selection) onSelection;

  @override
  Widget build(BuildContext context) {
    switch (state) {
      case Attending.yes:
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            children: [
              ElevatedButton(
                style: ElevatedButton.styleFrom(elevation: 0),
                onPressed: () => onSelection(Attending.yes),
                child: const Text('YES'),
              ),
              const SizedBox(width: 8),
              TextButton(
                onPressed: () => onSelection(Attending.no),
                child: const Text('NO'),
              ),
            ],
          ),
        );
      case Attending.no:
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            children: [
              TextButton(
                onPressed: () => onSelection(Attending.yes),
                child: const Text('YES'),
              ),
              const SizedBox(width: 8),
              ElevatedButton(
                style: ElevatedButton.styleFrom(elevation: 0),
                onPressed: () => onSelection(Attending.no),
                child: const Text('NO'),
              ),
            ],
          ),
        );
      default:
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            children: [
              StyledButton(
                onPressed: () => onSelection(Attending.yes),
                child: const Text('YES'),
              ),
              const SizedBox(width: 8),
              StyledButton(
                onPressed: () => onSelection(Attending.no),
                child: const Text('NO'),
              ),
            ],
          ),
        );
    }
  }
}

Next, you need to update HomePage 's build method to take advantage of YesNoSelection , enabling a logged in user to nominate if they are attending. You will also display the number of attendees for this event.

lib/main.dart

Consumer<ApplicationState>(
  builder: (context, appState, _) => Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      // Add from here
      if (appState.attendees >= 2)
        Paragraph('${appState.attendees} people going')
      else if (appState.attendees == 1)
        const Paragraph('1 person going')
      else
        const Paragraph('No one going'),
      // To here.
      if (appState.loginState == ApplicationLoginState.loggedIn) ...[
        // Add from here
        YesNoSelection(
          state: appState.attending,
          onSelection: (attending) => appState.attending = attending,
        ),
        // To here.
        const Header('Discussion'),
        GuestBook(
          addMessage: (message) =>
              appState.addMessageToGuestBook(message),
          messages: appState.guestBookMessages,
        ),
      ],
    ],
  ),
),

Add rules

Because you already have some rules set up, the new data that you're adding with the buttons is going to be rejected. You'll need to update the rules to allow adding to the attendees collection.

For the attendees collection, since you used the Authentication UID as the document name, you can grab it and verify that the submitter's uid is the same as the document they are writing. You'll allow everyone to read the attendees list (since there is no private data there), but only the creator should be able to update it.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // ... //
    match /attendees/{userId} {
      allow read: if true;
      allow write: if request.auth.uid == userId;
    }
  }
}

Add validation rules

Add data validation to make sure that all of the expected fields are present in the document:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // ... //
    match /attendees/{userId} {
      allow read: if true;
      allow write: if request.auth.uid == userId
          && "attending" in request.resource.data;

    }
  }
}

(Optional) You can now view the results of clicking the buttons. Go to your Cloud Firestore dashboard in the Firebase console.

App preview

10.おめでとうございます!

You've used Firebase to build an interactive, real-time web application!

What we've covered

  • Firebase Authentication
  • Cloud Firestore
  • Firebase Security Rules

Next steps

  • Want to learn more about other Firebase products? Maybe you want to store image files that users upload? Or send notifications to your users? Check out the Firebase documentation . Want to learn more about Flutter plugins for Firebase? Check out FlutterFire for more information.
  • Want to learn more about Cloud Firestore? Maybe you want to learn about subcollections and transactions? Head over to the Cloud Firestore web codelab for a codelab that goes into more depth on Cloud Firestore. Or check out this YouTube series to get to know Cloud Firestore !

Learn more

How did it go?

We would love your feedback! Please fill out a (very) short form here .