Firebase for Flutter を理解する

1. 始める前に

この Codelab では、Firebase の基本をいくつか学び、Android および iOS 向けの Flutter モバイルアプリを作成します。

前提条件

学習内容

  • Android、iOS、ウェブ、macOS で Flutter を使用してイベントの出欠確認やゲストブックのチャットアプリを作成する方法。
  • Firebase Authentication でユーザーを認証し、Firestore とデータを同期する方法。

Android のアプリのホーム画面

iOS 版のアプリのホーム画面

必要なもの

以下のデバイスのいずれか:

  • パソコンに接続され、デベロッパー モードに設定された物理デバイス(Android または iOS)
  • iOS シミュレータ(Xcode ツールが必要です)。
  • Android Emulator(Android Studio でセットアップが必要)。

また、次のものも必要です。

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

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

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

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

flutter-codelabs ディレクトリには、Codelab のコレクションのコードが含まれています。この Codelab のコードは 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 ミートアップ アプリで構成される Codelab のスターター コードが含まれています。

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

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

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

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

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

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

lib/src/widgets.dart ファイルのヘルパー ウィジェットを HeaderParagraphIconAndDetail の形式で使用します。これらのウィジェットは、重複するコードを排除して、HomePage で説明されているページ レイアウトを見やすくします。これにより、一貫したデザインも実現します。

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

Android のアプリのホーム画面

iOS 版のアプリのホーム画面

ウェブ上のアプリのホーム画面

macOS のアプリのホーム画面

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

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

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

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

4395e4e67c08043a.png

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

b7138cde5f2c7b61.png

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

このアプリは、ウェブアプリで利用可能な次の Firebase プロダクトを使用します。

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

一部のプロダクトは、特別な構成が必要な場合や、Firebase コンソールで有効にする必要があります。

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

  1. Firebase コンソールの [プロジェクトの概要] ペインで、[ビルド] メニューを開きます。
  2. [Authentication] > [Get Started] > [Sign-in method] > [Email/Password] > [Enable] > [Save] をクリックします。

58e3e3e23c2f16a4.png

Firestore を有効にする

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

Firestore を有効にします。

  • [Build] メニューで、[Firestore Database] > [Create database] をクリックします。

99e8429832d23fa3.png

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

6be00e26c72ea032.png

  1. [次へ] をクリックし、データベースのロケーションを選択します。デフォルトを使用できます。後でロケーションを変更することはできません。

278656eefcfb0216.png

  1. [有効にする] をクリックします。

4. Firebase を構成する

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

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

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

依存関係を構成する

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

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

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

$ flutter pub add firebase_auth

firebase_auth パッケージを使用すると、Authentication との統合が可能になります。

$ 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 Runner のプロジェクトを構成する必要もあります。また、ビジネス ロジックを表示ロジックから分離できるようにする 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、ウェブなど)を決定します。
  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. 出欠確認機能を追加する

Firebase をアプリに追加したので、Authentication にユーザーを登録する RSVP ボタンを作成できます。Android ネイティブ、iOS ネイティブ、ウェブ用にビルド済みの FirebaseUI Auth パッケージがありますが、Flutter 用にはこの機能を作成する必要があります。

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

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 パッケージを pull し、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. FirebaseUI に用意されているさまざまな画面へのナビゲーションを処理するように、GoRouter 構成を作成してアプリを更新します。

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 クラスのビルドメソッドで、アプリの状態を 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 ウィジェットでラップします。コンシューマー ウィジェットは、アプリの状態が変化したときに provider パッケージを使用してツリーの一部を再ビルドできる通常の方法です。AuthFunc ウィジェットは、テストする補助ウィジェットです。

認証フローをテストする

cdf2d25e436bd48d.png

  1. アプリで [出欠確認] ボタンをタップして 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 版アプリのホーム画面

チャットを統合したウェブ版アプリのホーム画面

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

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

FirebaseAuth.instance.currentUser.uid は、自動生成された一意の ID への参照です。これは、すべてのログイン ユーザーに Authentication によって与えられます。

  • 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 にエントリを追加するためのコードがあるとします。あとは両者を接続するだけです。

  • 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 コンソールの [Database] ダッシュボードでは、追加したメッセージが 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 版アプリのホーム画面

チャットを統合したウェブ版アプリのホーム画面

チャットが統合された 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 のアクセサ セクションに次の行を追加して、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'),
              ),
            ],
          ),
        );
    }
  }
}

[Yes] も [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 版のアプリのホーム画面

ウェブ上のアプリのホーム画面

macOS のアプリのホーム画面

10. お疲れさまでした

Firebase を使用して、インタラクティブなリアルタイム ウェブアプリを作成しました。

詳細