FlutterのFirebaseを知る

1.始める前に

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

前提条件

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

作成するもの

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

必要なもの

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

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

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

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

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

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

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

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

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

gh repo clone flutter/codelabs flutter-codelabs

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

アプリのプレビュー

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

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

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

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

4395e4e67c08043a.png

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

b7138cde5f2c7b61.png

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

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

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

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

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

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

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

CloudFirestoreを有効にする

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

CloudFirestoreを有効にする:

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

4.Firebaseの構成

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

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

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

依存関係を構成する

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

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

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

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

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

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

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

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

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

flutterfireインストール

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

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

$ dart pub global activate flutterfire_cli

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

アプリの構成

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

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

$ flutterfire configure

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

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

macOSを構成する

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

macos / Runner / DebugProfile.entitlements

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

macos / Runner / Release.entitlements

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

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

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

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

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

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

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

lib / main.dart

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

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

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

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

lib / main.dart

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

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

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

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

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

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

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

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

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

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

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

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

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

認証フローの統合

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

lib / main.dart

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

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

lib / main.dart

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

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

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

認証フローのテスト

cdf2d25e436bd48d.png

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

2a2cd6d69d172369.png

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

e5e65065dba36b54.png

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

fbb3ea35fb4f67a.png

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

4ed811a25b0cf816.png

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

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

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

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

データ・モデル

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

7c20dc8424bb1d84.png

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

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

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

lib / main.dart

import 'dart:async';                                    // new

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

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

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

lib / main.dart

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

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

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

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

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

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

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

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

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

アプリのプレビュー

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

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

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

lib / main.dart

class ApplicationState extends ChangeNotifier {

  // Current content of ApplicationState elided ...

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

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

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

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

lib / main.dart

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

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

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

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

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

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

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

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

Firebaseコンソール

713870af0b3b63c.png

7.メッセージを読む

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

メッセージを同期する

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

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

lib / main.dart

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

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

lib / main.dart

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

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

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

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

lib / main.dart

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

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

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

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

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

lib / main.dart

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

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

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

lib / main.dart

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

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

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

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

lib / main.dart

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

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

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

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

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

アプリpレビュー

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

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

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

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

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

7767a2d2e64e7275.png

コレクションを特定する

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

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

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

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

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

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

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

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

検証ルールを追加する

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

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

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

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

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

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

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

lib / main.dart

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

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

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

lib / main.dart

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

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

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

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

lib / main.dart

enum Attending { yes, no, unknown }

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

lib / main.dart

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

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

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

lib / main.dart

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

ルールを追加する

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

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

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

検証ルールを追加する

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

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

    }
  }
}

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

アプリのプレビュー

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

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

私たちがカバーしたこと

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

次のステップ

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

もっと詳しく知る

どうだった?

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