Firebase for Flutter を理解する

1. 始める前に

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

前提条件

学べること

  • Flutter を使用して Android、iOS、Web、macOS 上でイベントの RSVP およびゲストブック チャット アプリを構築する方法。
  • Firebase Authentication でユーザーを認証し、Firestore とデータを同期する方法。

Androidのアプリのホーム画面

iOSのアプリのホーム画面

必要なもの

次のいずれかのデバイス:

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

次のものも必要です。

  • Google Chrome などの任意のブラウザ。
  • Android StudioVisual Studio Codeなど、Dart および Flutter プラグインで構成された任意の IDE またはテキスト エディター。
  • Flutterの最新のstableバージョン、またはエッジでの生活を楽しみたい場合はbeta
  • Firebase プロジェクトの作成と管理用の Google アカウント。
  • Firebase CLI がGoogle アカウントにログインしました。

2. サンプルコードを入手する

GitHub からプロジェクトの初期バージョンをダウンロードします。

  1. コマンド ラインから、 flutter-codelabsディレクトリにGitHub リポジトリのクローンを作成します。
git clone https://github.com/flutter/codelabs.git flutter-codelabs

flutter-codelabsディレクトリには、コードラボのコレクションのコードが含まれています。このコードラボのコードはflutter-codelabs/firebase-get-to-know-flutterディレクトリにあります。このディレクトリには、各ステップの終了時にプロジェクトがどのように見えるかを示す一連のスナップショットが含まれています。たとえば、あなたは 2 番目のステップにいます。

  1. 2 番目のステップで一致するファイルを見つけます。
cd flutter-codelabs/firebase-get-to-know-flutter/step_02

先にスキップしたり、ステップの後にどのように表示されるかを確認したい場合は、興味のあるステップにちなんで名付けられたディレクトリを調べてください。

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

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

作業が必要なファイルを見つける

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

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

必要に応じてファイルを追加して、アプリの残りの部分をビルドします。

lib/main.dartファイルを確認します。

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

lib/src/widgets.dartファイルのヘルパー ウィジェットをHeaderParagraphおよびIconAndDetailの形式で使用します。これらのウィジェットは重複したコードを排除し、 HomePageで説明されているページ レイアウトの乱雑さを減らします。これにより、一貫した外観と操作性も実現します。

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

Androidのアプリのホーム画面

iOSのアプリのホーム画面

Web上のアプリのホーム画面

macOS のアプリのホーム画面

3. Firebase プロジェクトを作成して構成する

イベント情報の表示はゲストにとっては便利ですが、それ自体は誰にとってもあまり役に立ちません。アプリにいくつかの動的な機能を追加する必要があります。これを行うには、Firebase をアプリに接続する必要があります。 Firebase を使い始めるには、Firebase プロジェクトを作成して構成する必要があります。

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

  1. Firebaseにサインインします。
  2. コンソールで、 「プロジェクトの追加」または「プロジェクトの作成」をクリックします。
  3. [プロジェクト名]フィールドに「Firebase-Flutter-Codelab」と入力し、 [続行]をクリックします。

4395e4e67c08043a.png

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

b7138cde5f2c7b61.png

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

このアプリは、ウェブアプリで利用できる次の Firebase 製品を使用します。

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

これらの製品の中には、特別な構成が必要な場合や、Firebase コンソールで有効にする必要があるものがあります。

電子メールのサインイン認証を有効にする

  1. Firebase コンソールの[プロジェクト概要]ペインで、 [ビルド]メニューを展開します。
  2. [認証] > [開始] > [サインイン方法] > [電子メール/パスワード] > [有効にする] > [保存]をクリックします。

58e3e3e23c2f16a4.png

Firestoreを有効にする

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

Firestore を有効にする:

  • [構築]メニューで、 [Firestore データベース] > [データベースの作成]をクリックします。

99e8429832d23fa3.png

  1. [テスト モードで開始]を選択し、セキュリティ ルールに関する免責事項をお読みください。テスト モードでは、開発中にデータベースに自由に書き込むことができます。

6be00e26c72ea032.png

  1. 「次へ」をクリックし、データベースの場所を選択します。デフォルトのまま使用できます。後で場所を変更することはできません。

278656eefcfb0216.png

  1. 「有効にする」をクリックします。

4. Firebaseの構成

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

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

Flutter アプリの最上位ディレクトリには、 androidiosmacos 、およびwebサブディレクトリがあり、それぞれ iOS と Android のプラットフォーム固有の構成ファイルを保持します。

依存関係を構成する

このアプリで使用する 2 つの Firebase 製品 (Authentication と Firestore) のFlutterFireライブラリを追加する必要があります。

  • コマンドラインから、次の依存関係を追加します。
$ flutter pub add firebase_core

firebase_coreパッケージは、すべての Firebase Flutter プラグインに必要な共通コードです。

$ flutter pub add firebase_auth

firebase_authパッケージにより、認証との統合が可能になります。

$ flutter pub add cloud_firestore

cloud_firestoreパッケージを使用すると、 Firestore データ ストレージへのアクセスが可能になります。

$ flutter pub add provider

firebase_ui_authパッケージは、開発者の認証フローの速度を向上させるためのウィジェットとユーティリティのセットを提供します。

$ flutter pub add firebase_ui_auth

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

FlutterFire CLI をインストールする

FlutterFire CLI は、基盤となる Firebase CLI に依存します。

  1. Firebase CLI をまだインストールしていない場合は、マシンにインストールします。
  2. FlutterFire CLI をインストールします。
$ dart pub global activate flutterfire_cli

flutterfireコマンドをインストールすると、世界中で使用できるようになります。

アプリを設定する

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

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

$ flutterfire configure

設定コマンドは、次のプロセスを案内します。

  1. .firebasercファイルに基づいて、または Firebase コンソールから 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>

詳細については、 「Flutter のデスクトップ サポート」を参照してください。

5. RSVP 機能を追加する

Firebase をアプリに追加したので、ユーザーをAuthenticationに登録するRSVPボタンを作成できます。 Android ネイティブ、iOS ネイティブ、Web の場合は、事前に構築されたFirebaseUI Authパッケージがありますが、Flutter 用にこの機能を構築する必要があります。

前に取得したプロジェクトには、ほとんどの認証フローのユーザー インターフェイスを実装するウィジェットのセットが含まれていました。ビジネス ロジックを実装して、認証をアプリと統合します。

Providerパッケージを使用してビジネス ロジックを追加する

providerパッケージを使用して、アプリの Flutter ウィジェットのツリー全体で一元化されたアプリ状態オブジェクトを利用できるようにします。

  1. 次の内容を含むapp_state.dartという名前の新しいファイルを作成します。

lib/app_state.dart

import 'package:firebase_auth/firebase_auth.dart'
    hide EmailAuthProvider, PhoneAuthProvider;
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_ui_auth/firebase_ui_auth.dart';
import 'package:flutter/material.dart';

import 'firebase_options.dart';

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

  bool _loggedIn = false;
  bool get loggedIn => _loggedIn;

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

    FirebaseUIAuth.configureProviders([
      EmailAuthProvider(),
    ]);

    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loggedIn = true;
      } else {
        _loggedIn = false;
      }
      notifyListeners();
    });
  }
}

importステートメントは、Firebase Core と Auth を導入し、アプリ状態オブジェクトをウィジェット ツリー全体で利用できるようにするproviderパッケージを取り込み、 firebase_ui_authパッケージの認証ウィジェットを含めます。

このApplicationStateアプリケーション状態オブジェクトには、このステップに対する 1 つの主な役割があります。それは、認証済み状態への更新があったことをウィジェット ツリーに警告することです。

プロバイダーを使用するのは、ユーザーのログイン ステータスの状態をアプリに伝達する場合のみです。ユーザーがログインできるようにするには、 firebase_ui_authパッケージで提供される UI を使用します。これは、アプリでログイン画面をすばやくブートストラップする優れた方法です。

認証フローを統合する

  1. lib/main.dartファイルの先頭にあるインポートを変更します。

lib/main.dart

import 'package:firebase_ui_auth/firebase_ui_auth.dart'; // new
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';               // new
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';                 // new

import 'app_state.dart';                                 // new
import 'home_page.dart';
  1. アプリの状態をアプリの初期化に接続し、認証フローをHomePageに追加します。

lib/main.dart

void main() {
  // Modify from here...
  WidgetsFlutterBinding.ensureInitialized();

  runApp(ChangeNotifierProvider(
    create: (context) => ApplicationState(),
    builder: ((context, child) => const App()),
  ));
  // ...to here.
}

main()関数を変更すると、プロバイダー パッケージがChangeNotifierProviderウィジェットを使用したアプリ状態オブジェクトのインスタンス化を担当するようになります。この特定のproviderクラスを使用するのは、アプリ状態オブジェクトがChangeNotifierクラスを拡張するためです。これにより、 providerパッケージは依存ウィジェットを再表示するタイミングを認識できるようになります。

  1. GoRouter構成を作成して、FirebaseUI が提供するさまざまな画面へのナビゲーションを処理できるようにアプリを更新します。

lib/main.dart

// Add GoRouter configuration outside the App class
final _router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomePage(),
      routes: [
        GoRoute(
          path: 'sign-in',
          builder: (context, state) {
            return SignInScreen(
              actions: [
                ForgotPasswordAction(((context, email) {
                  final uri = Uri(
                    path: '/sign-in/forgot-password',
                    queryParameters: <String, String?>{
                      'email': email,
                    },
                  );
                  context.push(uri.toString());
                })),
                AuthStateChangeAction(((context, state) {
                  final user = switch (state) {
                    SignedIn state => state.user,
                    UserCreated state => state.credential.user,
                    _ => null
                  };
                  if (user == null) {
                    return;
                  }
                  if (state is UserCreated) {
                    user.updateDisplayName(user.email!.split('@')[0]);
                  }
                  if (!user.emailVerified) {
                    user.sendEmailVerification();
                    const snackBar = SnackBar(
                        content: Text(
                            'Please check your email to verify your email address'));
                    ScaffoldMessenger.of(context).showSnackBar(snackBar);
                  }
                  context.pushReplacement('/');
                })),
              ],
            );
          },
          routes: [
            GoRoute(
              path: 'forgot-password',
              builder: (context, state) {
                final arguments = state.uri.queryParameters;
                return ForgotPasswordScreen(
                  email: arguments['email'],
                  headerMaxExtent: 200,
                );
              },
            ),
          ],
        ),
        GoRoute(
          path: 'profile',
          builder: (context, state) {
            return ProfileScreen(
              providers: const [],
              actions: [
                SignedOutAction((context) {
                  context.pushReplacement('/');
                }),
              ],
            );
          },
        ),
      ],
    ),
  ],
);
// end of GoRouter configuration

// Change MaterialApp to MaterialApp.router and add the routerConfig
class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Firebase Meetup',
      theme: ThemeData(
        buttonTheme: Theme.of(context).buttonTheme.copyWith(
              highlightColor: Colors.deepPurple,
            ),
        primarySwatch: Colors.deepPurple,
        textTheme: GoogleFonts.robotoTextTheme(
          Theme.of(context).textTheme,
        ),
        visualDensity: VisualDensity.adaptivePlatformDensity,
        useMaterial3: true,
      ),
      routerConfig: _router, // new
    );
  }
}

各画面には、認証フローの新しい状態に基づいて、異なるタイプのアクションが関連付けられています。認証でほとんどの状態が変化した後は、ホーム画面であっても、プロファイルなどの別の画面であっても、希望の画面に戻ることができます。

  1. HomePageクラスの build メソッドで、アプリの状態をAuthFuncウィジェットと統合します。

lib/home_page.dart

import 'package:firebase_auth/firebase_auth.dart' // new
    hide EmailAuthProvider, PhoneAuthProvider;    // new
import 'package:flutter/material.dart';           // new
import 'package:provider/provider.dart';          // new

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

class HomePage extends StatelessWidget {
  const HomePage({super.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, _) => AuthFunc(
                loggedIn: appState.loggedIn,
                signOut: () {
                  FirebaseAuth.instance.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!',
          ),
        ],
      ),
    );
  }
}

AuthFuncウィジェットをインスタンス化し、それをConsumerウィジェットでラップします。 Consumer ウィジェットは、アプリの状態が変化したときにproviderパッケージを使用してツリーの一部を再構築する通常の方法です。 AuthFuncウィジェットは、テストする補助ウィジェットです。

認証フローをテストする

cdf2d25e436bd48d.png

  1. アプリで、 [RSVP]ボタンをタップしてSignInScreenを開始します。

2a2cd6d69d172369.png

  1. メールアドレスを入力。すでに登録している場合は、パスワードの入力を求めるプロンプトが表示されます。それ以外の場合は、登録フォームに記入するよう求められます。

e5e65065dba36b54.png

  1. エラー処理フローを確認するには、6 文字未満のパスワードを入力してください。登録されている場合は、代わりに のパスワードが表示されます。
  2. エラー処理フローを確認するには、間違ったパスワードを入力してください。
  3. 正しいパスワードを入力してください。ログイン エクスペリエンスが表示され、ユーザーはログアウトできるようになります。

4ed811a25b0cf816.png

6. Firestore にメッセージを書き込む

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

ユーザーがアプリに書いたチャット メッセージを保存するには、 Firestoreを使用します。

データ・モデル

Firestore は NoSQL データベースであり、データベースに保存されるデータはコレクション、ドキュメント、フィールド、サブコレクションに分割されます。チャットの各メッセージを、最上位のコレクションであるguestbookコレクションにドキュメントとして保存します。

7c20dc8424bb1d84.png

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

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

  1. guest_book.dartという名前の新しいファイルを作成し、 GuestBookステートフル ウィジェットを追加して、メッセージ フィールドと送信ボタンの UI 要素を構築します。

lib/guest_book.dart

import 'dart:async';

import 'package:flutter/material.dart';

import 'src/widgets.dart';

class GuestBook extends StatefulWidget {
  const GuestBook({required this.addMessage, super.key});

  final FutureOr<void> Function(String message) addMessage;

  @override
  State<GuestBook> 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を使用してフォームの背後にあるフォーム状態にアクセスします。キーとその使用方法の詳細については、 「キーを使用する場合」を参照してください。

また、ウィジェットのレイアウト方法にも注目してください。 TextFormFieldを含むRowと、 Rowを含むStyledButtonがあります。また、 TextFormFieldExpandedウィジェットでラップされているため、 TextFormField行内の余分なスペースを強制的に埋めることにも注意してください。これが必要な理由をよりよく理解するには、 「制約について」を参照してください。

ユーザーがゲスト ブックに追加するテキストを入力できるウィジェットが完成したので、それを画面上に表示する必要があります。

  1. 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)),

ウィジェットを表示するにはこれで十分ですが、何か役立つことをするには十分ではありません。このコードを機能させるには、すぐにこのコードを更新します。

アプリのプレビュー

チャットが統合された Android 上のアプリのホーム画面

チャットを統合した iOS 上のアプリのホーム画面

チャット統合を備えた Web 上のアプリのホーム画面

チャットが統合された macOS 上のアプリのホーム画面

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

FirebaseAuth.instance.currentUser.uid 、認証によってログインしているすべてのユーザーに与えられる自動生成された一意の ID への参照であることに注意してください。

  • lib/app_state.dartファイルに、 addMessageToGuestBookメソッドを追加します。次のステップで、この機能をユーザー インターフェイスに接続します。

lib/app_state.dart

import 'package:cloud_firestore/cloud_firestore.dart'; // new
import 'package:firebase_auth/firebase_auth.dart'
    hide EmailAuthProvider, PhoneAuthProvider;
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_ui_auth/firebase_ui_auth.dart';
import 'package:flutter/material.dart';

import 'firebase_options.dart';

class ApplicationState extends ChangeNotifier {

  // Current content of ApplicationState elided ...

  // Add from here...
  Future<DocumentReference> addMessageToGuestBook(String message) {
    if (!_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 があり、そのエントリを Firestore に追加するコードがあります。あとは 2 つを接続するだけです。

  • lib/home_page.dartファイルで、 HomePageウィジェットに次の変更を加えます。

lib/home_page.dart

import 'package:firebase_auth/firebase_auth.dart'
    hide EmailAuthProvider, PhoneAuthProvider;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import 'app_state.dart';
import 'guest_book.dart';                         // new
import 'src/authentication.dart';
import 'src/widgets.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, _) => AuthFunc(
                loggedIn: appState.loggedIn,
                signOut: () {
                  FirebaseAuth.instance.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.loggedIn) ...[
                  const Header('Discussion'),
                  GuestBook(
                    addMessage: (message) =>
                        appState.addMessageToGuestBook(message),
                  ),
                ],
              ],
            ),
          ),
          // ...to here.
        ],
      ),
    );
  }
}

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

メッセージ送信のテスト

  1. 必要に応じて、アプリにサインインします。
  2. Hey there!などのメッセージを入力します。をクリックし、 [送信]をクリックします。

このアクションにより、メッセージが Firestore データベースに書き込まれます。ただし、データの取得を実装する必要があるため、実際の Flutter アプリにはメッセージが表示されません。これは次のステップで実行します。ただし、Firebase コンソールのデータベースダッシュボードでは、 guestbookコレクションに追加されたメッセージを確認できます。より多くのメッセージを送信すると、 guestbookコレクションにさらに多くのドキュメントが追加されます。たとえば、次のコード スニペットを参照してください。

713870af0b3b63c.png

7. メッセージを読む

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

メッセージを同期する

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

  1. 新しいファイルguest_book_message.dartを作成し、次のクラスを追加して、Firestore に保存するデータの構造化ビューを公開します。

lib/guest_book_message.dart

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

  final String name;
  final String message;
}
  1. lib/app_state.dartファイルに、次のインポートを追加します。

lib/app_state.dart

import 'dart:async';                                     // new

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart'
    hide EmailAuthProvider, PhoneAuthProvider;
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_ui_auth/firebase_ui_auth.dart';
import 'package:flutter/material.dart';

import 'firebase_options.dart';
import 'guest_book_message.dart';                        // new
  1. 状態とゲッターを定義するApplicationStateのセクションに、次の行を追加します。

lib/app_state.dart

  bool _loggedIn = false;
  bool get loggedIn => _loggedIn;

  // Add from here...
  StreamSubscription<QuerySnapshot>? _guestBookSubscription;
  List<GuestBookMessage> _guestBookMessages = [];
  List<GuestBookMessage> get guestBookMessages => _guestBookMessages;
  // ...to here.
  1. ApplicationStateの初期化セクションに、ユーザーがログインするときにドキュメント コレクションに対するクエリをサブスクライブし、ログアウト時にサブスクライブを解除する次の行を追加します。

lib/app_state.dart

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

    FirebaseUIAuth.configureProviders([
      EmailAuthProvider(),
    ]);
    
    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loggedIn = true;
        _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();
        });
      } else {
        _loggedIn = false;
        _guestBookMessages = [];
        _guestBookSubscription?.cancel();
      }
      notifyListeners();
    });
  }

このセクションは、 guestbookコレクションに対するクエリを作成し、このコレクションの購読と購読解除を処理する場所であるため、重要です。ストリームをリッスンして、 guestbookコレクション内のメッセージのローカル キャッシュを再構築し、後でサブスクライブを解除できるように、このサブスクリプションへの参照も保存します。ここでは多くのことが行われているため、より明確なメンタル モデルを取得するには、デバッガーで調査して何が起こっているかを検査する必要があります。詳細については、 「Firestore でリアルタイム更新を取得する」を参照してください。

  1. lib/guest_book.dartファイルに、次のインポートを追加します。
import 'guest_book_message.dart';
  1. GuestBookウィジェットで、この変化する状態をユーザー インターフェイスに接続するための設定の一部としてメッセージのリストを追加します。

lib/guest_book.dart

class GuestBook extends StatefulWidget {
  // Modify the following line:
  const GuestBook({
    super.key, 
    required this.addMessage, 
    required this.messages,
  });

  final FutureOr<void> Function(String message) addMessage;
  final List<GuestBookMessage> messages; // new

  @override
  _GuestBookState createState() => _GuestBookState();
}
  1. _GuestBookStateで、 buildメソッドを次のように変更して、この構成を公開します。

lib/guest_book.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を生成します。

  1. HomePageの本文を更新して、新しいmessagesパラメータを使用してGuestBookを正しく構築します。

lib/home_page.dart

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

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

Firestore は、データベースに登録しているクライアントとデータを自動的かつ即座に同期します。

メッセージの同期をテストします。

  1. アプリで、データベース内で以前に作成したメッセージを見つけます。
  2. 新しいメッセージを書きます。それらは即座に現れます。
  3. 複数のウィンドウまたはタブでワークスペースを開きます。メッセージはウィンドウとタブ間でリアルタイムに同期されます。
  4. オプション: Firebase コンソールの[データベース]メニューで、手動でメッセージを削除、変更、または新しいメッセージを追加します。すべての変更は UI に表示されます。

おめでとう!アプリで Firestore ドキュメントを読みました。

アプリのプレビュー

チャットが統合された Android 上のアプリのホーム画面

チャットを統合した iOS 上のアプリのホーム画面

チャット統合を備えた Web 上のアプリのホーム画面

チャットが統合された macOS 上のアプリのホーム画面

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

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

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

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

  1. Firebase コンソールの[開発]メニューで、 [データベース] > [ルール]をクリックします。次のデフォルトのセキュリティ ルールと、ルールが公開されているという警告が表示されます。

7767a2d2e64e7275.png

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

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 を持っているかどうかを確認できます。

  1. 読み取りおよび書き込みルールをルール セットに追加します。
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;
    }
  }
}

現在、サインインしているユーザーのみがゲスト ブック内のメッセージを読むことができますが、メッセージを編集できるのはメッセージの作成者だけです。

  1. データ検証を追加して、予期されるすべてのフィールドがドキュメント内に存在することを確認します。
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 つ目は、参加者数のカウンターです。

  1. lib/app_state.dartファイルで、 ApplicationStateの accessors セクションに次の行を追加して、UI コードがこの状態と対話できるようにします。

lib/app_state.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});
  }
}
  1. ApplicationStateinit()メソッドを次のように更新します。

lib/app_state.dart

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

    FirebaseUIAuth.configureProviders([
      EmailAuthProvider(),
    ]);

    // 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) {
        _loggedIn = true;
        _emailVerified = user.emailVerified;
        _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 {
        _loggedIn = false;
        _emailVerified = false;
        _guestBookMessages = [];
        _guestBookSubscription?.cancel();
        _attendingSubscription?.cancel(); // new
      }
      notifyListeners();
    });
  }

このコードは、出席者の数を判断するための常時サブスクライブ クエリと、ユーザーが出席しているかどうかを判断するためにユーザーがログインしている間のみアクティブになる 2 番目のクエリを追加します。

  1. lib/app_state.dartファイルの先頭に次の列挙を追加します。

lib/app_state.dart

enum Attending { yes, no, unknown }
  1. 新しいファイルyes_no_selection.dartを作成し、ラジオ ボタンのように機能する新しいウィジェットを定義します。

lib/yes_no_selection.dart

import 'package:flutter/material.dart';

import 'app_state.dart';
import 'src/widgets.dart';

class YesNoSelection extends StatelessWidget {
  const YesNoSelection(
      {super.key, 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: [
              FilledButton(
                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),
              FilledButton(
                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'),
              ),
            ],
          ),
        );
    }
  }
}

「はい」も「いいえ」も選択されていない不定状態で開始されます。ユーザーが出席するかどうかを選択すると、そのオプションが塗りつぶされたボタンで強調表示され、他のオプションはフラットなレンダリングで隠れます。

  1. HomePagebuild()メソッドを更新してYesNoSelectionを利用し、ログイン ユーザーが出席するかどうかを指定できるようにし、イベントの出席者数を表示できるようにします。

lib/home_page.dart

Consumer<ApplicationState>(
  builder: (context, appState, _) => Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      // Add from here...
      switch (appState.attendees) {
        1 => const Paragraph('1 person going'),
        >= 2 => Paragraph('${appState.attendees} people going'),
        _ => const Paragraph('No one going'),
      },
      // ...to here.
      if (appState.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コレクションへの追加を許可するには、ルールを更新する必要があります。

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

これにより、出席者リストには個人データが含まれないため、誰でも参加者リストを読むことができますが、作成者のみがそれを更新できます。

  1. データ検証を追加して、予期されるすべてのフィールドがドキュメント内に存在することを確認します。
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;

    }
  }
}
  1. オプション: アプリでボタンをクリックすると、Firebase コンソールの Firestore ダッシュボードに結果が表示されます。

アプリのプレビュー

Androidのアプリのホーム画面

iOSのアプリのホーム画面

Web上のアプリのホーム画面

macOS のアプリのホーム画面

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

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

もっと詳しく知る