FlutterのFirebaseを知る

コレクションでコンテンツを整理 必要に応じて、コンテンツの保存と分類を行います。

1. 始める前に

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

前提条件

学習内容

  • Flutter を使用して、Android、iOS、Web、macOS でイベント出欠確認とゲストブック チャット アプリを構築する方法。
  • 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ディレクトリには、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ファイルのヘルパー ウィジェットをHeaderParagraph 、およびIconAndDetailの形式で使用します。これらのウィジェットは重複するコードを排除して、 HomePageで説明されているページ レイアウトの混乱を減らします。これにより、一貫したルック アンド フィールも実現します。

Android、iOS、Web、および 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 プロダクトを使用します。

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

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

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

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

58e3e3e23c2f16a4.png

Firestore を有効にする

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

Firestore を有効にします。

  • [ビルド] メニューで、[ Cloud 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パッケージにより、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 ランナー プロジェクトも構成する必要があります。また、表示ロジックからビジネス ロジックを分離できる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 用にこの機能をビルドする必要があります。

以前に取得したプロジェクトには、ほとんどの認証フローのユーザー インターフェイスを実装する一連のウィジェットが含まれていました。ビジネス ロジックを実装して、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パッケージを取り込み、 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) {
                  if (state is SignedIn || state is UserCreated) {
                    var user = (state is SignedIn)
                        ? state.user
                        : (state as UserCreated).credential.user;
                    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.queryParams;
                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. アプリで、 RSVPボタンをタップしてSignInScreenを開始します。

2a2cd6d69d172369.png

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

e5e65065dba36b54.png

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

4ed811a25b0cf816.png

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

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

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

データ・モデル

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

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は、Authentication がすべてのログイン ユーザーに与える自動生成された一意の 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. ボーナスステップ: 学んだことを実践する

出席者の RSVP ステータスを記録する

現在、あなたのアプリでは、ユーザーがイベントに興味を持っている場合にのみチャットを許可しています。また、誰かが来るかどうかを知る唯一の方法は、チャットでそう言ったときです。

このステップでは、組織化して、何人の人が来るかを人々に知らせます。アプリの状態にいくつかの機能を追加します。 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) {
        _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 つ目のクエリを追加して、ユーザーが出席しているかどうかを判断します。

  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: [
              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'),
              ),
            ],
          ),
        );
    }
  }
}

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

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

lib/home_page.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.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 を使用して、インタラクティブなリアルタイム ウェブアプリを構築しました。

もっと詳しく知る