FlutterのFirebaseを知る

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

1. 始める前に

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

前提条件

この Codelab は、読者が Flutter に精通しており、 Flutter SDKエディタがインストールされていることを前提としています。

作成するもの

この Codelab では、Flutter を使用して、Android、iOS、ウェブ、macOS でイベント出欠確認とゲストブック チャット アプリを構築します。 Firebase Authentication でユーザーを認証し、Cloud Firestore を使用してデータを同期します。

必要なもの

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

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

上記に加えて、次のものが必要です。

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

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

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

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

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

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

gh repo clone flutter/codelabs flutter-codelabs

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

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

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

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

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

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

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

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

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

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

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

lib/main.dartファイルの確認

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

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

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

アプリのプレビュー

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

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

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

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

4395e4e67c08043a.png

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

b7138cde5f2c7b61.png

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

構築中のアプリは、ウェブアプリで利用できるいくつかの Firebase プロダクトを使用しています。

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

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

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

ユーザーがウェブ アプリにサインインできるようにするには、この Codelab でメール/パスワードによるサインイン方法を使用します。

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

Cloud Firestore を有効にする

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

Cloud Firestore を有効にします。

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

4. Firebase の設定

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

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

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

依存関係を構成する

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

$ flutter pub add firebase_core

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

$ flutter pub add firebase_auth

firebase_authにより、Firebase の認証機能との統合が可能になります。

$ flutter pub add cloud_firestore

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

$ flutter pub add provider

firebase_ui_authパッケージは、特に認証フローで開発者の速度を上げるための一連のウィジェットとユーティリティを提供します。

$ flutter pub add firebase_ui_auth

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

flutterfireインストール

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

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

$ dart pub global activate flutterfire_cli

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

アプリの構成

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

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

$ flutterfire configure

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

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

macOS の構成

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

macos/Runner/DebugProfile.entitlements

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

macos/Runner/Release.entitlements

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

詳細については、資格とアプリ サンドボックスを参照してください。

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

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

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

プロバイダーを使用したビジネス ロジック

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

lib/main.dart

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

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

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

このアプリケーション状態オブジェクトApplicationStateは、このステップで 1 つの主な役割を果たします。これは、認証済み状態への更新があったことをウィジェット ツリーに警告することです。次のクラスをlib/main.dartの末尾に追加します。

lib/main.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();
    });
  }
}

ここではプロバイダーを使用して、ユーザーのログイン ステータスの状態をアプリに通知しています。ユーザーをログインさせるために、 firebase_ui_authによって提供される UI を使用します。これは、アプリケーションのログイン画面をすばやくブートストラップする優れた方法です。

認証フローの統合

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

lib/main.dart

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

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

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

Flutter に FirebaseUI を使用しているため、アプリを更新して、FirebaseUI が提供するさまざまな画面への移動を処理します。これを行うには、 initialRouteプロパティを追加し、 routesプロパティの下にルーティングできる優先スクリーンを追加します。変更は次のようになります。

lib/main.dart

class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      //Start adding here
      initialRoute: '/home',
      routes: {
        '/home': (context) {
          return const HomePage();
        },
        '/sign-in': ((context) {
          return SignInScreen(
            actions: [
              ForgotPasswordAction(((context, email) {
                Navigator.of(context)
                    .pushNamed('/forgot-password', arguments: {'email': email});
              })),
              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);
                  }
                  Navigator.of(context).pushReplacementNamed('/home');
                }
              })),
            ],
          );
        }),
        '/forgot-password': ((context) {
          final arguments = ModalRoute.of(context)?.settings.arguments
              as Map<String, dynamic>?;

          return ForgotPasswordScreen(
            email: arguments?['email'] as String,
            headerMaxExtent: 200,
          );
        }),
        '/profile': ((context) {
          return ProfileScreen(
            providers: [],
            actions: [
              SignedOutAction(
                ((context) {
                  Navigator.of(context).pushReplacementNamed('/home');
                }),
              ),
            ],
          );
        })
      },
      // end adding here
      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,
      ),
    );
  }
}

各画面には、認証フローの新しい状態に基づいて、異なるタイプのアクションが関連付けられています。認証でほとんどの状態が変化した後、それがホーム画面であろうと、プロファイルなどの別の画面であろうと、好みの画面に戻ることができます。最後に、 HomePageの build メソッドを更新して、アプリケーションの状態をAuthFuncと統合します。

lib/main.dart

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Consumer<ApplicationState>(
        builder: (context, appState, child) => 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

これは、ユーザーが RSVP ボタンをタップしてSignInScreenを開始できる認証フローの開始です。

2a2cd6d69d172369.png

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

e5e65065dba36b54.png

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

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

4ed811a25b0cf816.png

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

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

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

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

データ・モデル

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

7c20dc8424bb1d84.png

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

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

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

lib/main.dart

import 'dart:async';                                    // new

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

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

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

lib/main.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を使用します。キーの詳細とその使用方法については、 Flutter Widgets 101 エピソード「キーを使用する場合」を参照してください。

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

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

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

ウィジェットを表示するにはこれで十分ですが、有用なことを行うには十分ではありません。すぐにこのコードを更新して、機能するようにします。

アプリのプレビュー

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

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

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

lib/main.dart

class ApplicationState extends ChangeNotifier {

  // Current content of ApplicationState elided ...

  // Add from here
  Future<DocumentReference> addMessageToGuestBook(String message) {
    if (!_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 があり、エントリを Cloud Firestore に追加するコードがあります。あとは、この 2 つを一緒に配線するだけです。 lib/main.dartで、 HomePageウィジェットに次の変更を加えます。

lib/main.dart

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Firebase Meetup'),
      ),
      body: ListView(
        children: <Widget>[
          Image.asset('assets/codelab.png'),
          const SizedBox(height: 8),
          const IconAndDetail(Icons.calendar_today, 'October 30'),
          const IconAndDetail(Icons.location_city, 'San Francisco'),
          Consumer<ApplicationState>(
            builder: (context, appState, _) => 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!」などのメッセージを入力し、[送信] をクリックします。

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

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

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

Firebase コンソール

713870af0b3b63c.png

7. メッセージを読む

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

メッセージを同期する

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

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

lib/main.dart

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

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

lib/main.dart

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

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

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

lib/main.dart

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

    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コレクション内のメッセージのローカル キャッシュを再構築するストリームをリッスンし、後でサブスクリプションを解除できるように、このサブスクリプションへの参照も保存します。ここでは多くのことが行われています。より明確なメンタル モデルを得るには、デバッガーでいつ何が起こるかを調べてみる価値があります。

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

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

lib/main.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();
}

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

lib/main.dart

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

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

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

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

lib/main.dart

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

同期メッセージのテスト

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

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

おめでとう!アプリで Cloud Firestore ドキュメントを読み取っています。

アプリのレビュー

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

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

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

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

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

7767a2d2e64e7275.png

コレクションを特定する

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

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

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

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

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

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

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

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

検証ルールを追加する

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

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

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

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

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

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

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

lib/main.dart

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

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

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

lib/main.dart

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

    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 つ目のクエリを追加します。次に、 GuestBookMessage宣言の後に次の列挙を追加します。

lib/main.dart

enum Attending { yes, no, unknown }

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

lib/main.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'),
              ),
            ],
          ),
        );
    }
  }
}

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

lib/main.dart

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

ルールを追加

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

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

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

検証ルールを追加する

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

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

    }
  }
}

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

アプリのプレビュー

10.おめでとう!

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

カバーした内容

  • Firebase 認証
  • クラウド ファイアストア
  • Firebase セキュリティ ルール

次のステップ

  • 他の Firebase プロダクトについて詳しく知りたいですか?ユーザーがアップロードした画像ファイルを保存したいですか?または、ユーザーに通知を送信しますか? Firebase のドキュメントをご覧ください。 Firebase 用の Flutter プラグインについて詳しく知りたいですか?詳細については、 FlutterFireをご覧ください。
  • Cloud Firestore について詳しく知りたいですか?サブコレクションとトランザクションについて学びたいと思いませんか? Cloud Firestore について詳しく説明しているコードラボについては、 Cloud Firestore ウェブ コードラボをご覧ください。または、このYouTube シリーズをチェックして、Cloud Firestore について理解してください。

もっと詳しく知る

どうだった?

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