1. 始める前に
このコードラボでは、Android および iOS 用の Flutter モバイル アプリを作成するためのFirebaseの基本のいくつかを学びます。
前提条件
- Flutter についての知識
- フラッターSDK
- 好みのテキストエディター
学べること
- Flutter を使用して Android、iOS、Web、macOS 上でイベントの RSVP およびゲストブック チャット アプリを構築する方法。
- Firebase Authentication でユーザーを認証し、Firestore とデータを同期する方法。
必要なもの
次のいずれかのデバイス:
- コンピュータに接続され、開発者モードに設定されている物理的な Android または iOS デバイス。
- iOS シミュレーター ( Xcode ツールが必要)。
- Android エミュレーター ( Android Studioでのセットアップが必要です)。
次のものも必要です。
- Google Chrome などの任意のブラウザ。
- Android StudioやVisual Studio Codeなど、Dart および Flutter プラグインで構成された任意の IDE またはテキスト エディター。
- Flutterの最新の
stable
バージョン、またはエッジでの生活を楽しみたい場合はbeta
。 - Firebase プロジェクトの作成と管理用の Google アカウント。
-
Firebase
CLI がGoogle アカウントにログインしました。
2. サンプルコードを入手する
GitHub からプロジェクトの初期バージョンをダウンロードします。
- コマンド ラインから、
flutter-codelabs
ディレクトリにGitHub リポジトリのクローンを作成します。
git clone https://github.com/flutter/codelabs.git flutter-codelabs
flutter-codelabs
ディレクトリには、コードラボのコレクションのコードが含まれています。このコードラボのコードはflutter-codelabs/firebase-get-to-know-flutter
ディレクトリにあります。このディレクトリには、各ステップの終了時にプロジェクトがどのように見えるかを示す一連のスナップショットが含まれています。たとえば、あなたは 2 番目のステップにいます。
- 2 番目のステップで一致するファイルを見つけます。
cd flutter-codelabs/firebase-get-to-know-flutter/step_02
先に進みたい場合、またはステップの後にどのように表示されるかを確認したい場合は、関心のあるステップにちなんで名付けられたディレクトリを参照してください。
スターターアプリをインポートする
- 好みの IDE で
flutter-codelabs/firebase-get-to-know-flutter/step_02
ディレクトリを開くかインポートします。このディレクトリには、まだ機能していない Flutter ミートアップ アプリで構成されるコードラボのスターター コードが含まれています。
作業が必要なファイルを見つける
このアプリのコードは複数のディレクトリに分散されています。この機能の分割により、コードが機能ごとにグループ化されるため、作業が容易になります。
- 次のファイルを見つけます。
-
lib/main.dart
: このファイルには、メイン エントリ ポイントとアプリ ウィジェットが含まれています。 -
lib/home_page.dart
: このファイルにはホームページ ウィジェットが含まれています。 -
lib/src/widgets.dart
: このファイルには、アプリのスタイルの標準化に役立ついくつかのウィジェットが含まれています。スターターアプリの画面を構成します。 -
lib/src/authentication.dart
: このファイルには、Firebase メールベース認証のログイン ユーザー エクスペリエンスを作成するためのウィジェットのセットを備えた認証の部分的な実装が含まれています。認証フロー用のこれらのウィジェットはスターター アプリではまだ使用されていませんが、すぐに追加します。
-
必要に応じてファイルを追加して、アプリの残りの部分をビルドします。
lib/main.dart
ファイルを確認します。
このアプリはgoogle_fonts
パッケージを利用して、Roboto をアプリ全体のデフォルト フォントにします。 fonts.google.comを探索し、そこで見つけたフォントをアプリのさまざまな部分で使用できます。
lib/src/widgets.dart
ファイルのヘルパー ウィジェットをHeader
、 Paragraph
およびIconAndDetail
の形式で使用します。これらのウィジェットは重複したコードを排除し、 HomePage
で説明されているページ レイアウトの乱雑さを減らします。これにより、一貫した外観と操作性も実現します。
Android、iOS、Web、macOS でのアプリの表示は次のとおりです。
3. Firebase プロジェクトを作成して構成する
イベント情報の表示はゲストにとっては便利ですが、それ自体は誰にとってもあまり役に立ちません。アプリにいくつかの動的な機能を追加する必要があります。これを行うには、Firebase をアプリに接続する必要があります。 Firebase を使い始めるには、Firebase プロジェクトを作成して構成する必要があります。
Firebaseプロジェクトを作成する
- Firebaseにサインインします。
- コンソールで、 「プロジェクトの追加」または「プロジェクトの作成」をクリックします。
- [プロジェクト名]フィールドに「Firebase-Flutter-Codelab」と入力し、 [続行]をクリックします。
- プロジェクト作成オプションをクリックして進みます。プロンプトが表示されたら、Firebase の利用規約に同意しますが、このアプリでは Google アナリティクスを使用しないため、Google アナリティクスのセットアップはスキップします。
Firebase プロジェクトの詳細については、 「Firebase プロジェクトを理解する」を参照してください。
このアプリは、ウェブアプリで利用できる次の Firebase 製品を使用します。
- 認証:ユーザーがアプリにサインインできるようにします。
- Firestore:構造化データをクラウドに保存し、データが変更されたときに即座に通知を受け取ります。
- Firebase セキュリティ ルール:データベースを保護します。
これらの製品の中には、特別な構成が必要な場合や、Firebase コンソールで有効にする必要があるものがあります。
電子メールのサインイン認証を有効にする
- Firebase コンソールの[プロジェクト概要]ペインで、 [ビルド]メニューを展開します。
- [認証] > [開始] > [サインイン方法] > [電子メール/パスワード] > [有効にする] > [保存] をクリックします。
Firestoreを有効にする
このウェブアプリはFirestoreを使用してチャット メッセージを保存し、新しいチャット メッセージを受信します。
Firestore を有効にする:
- [ビルド]メニューで、 [Cloud Firestore] > [データベースの作成]をクリックします。
- [テスト モードで開始]を選択し、セキュリティ ルールに関する免責事項をお読みください。テスト モードでは、開発中にデータベースに自由に書き込むことができます。
- 「次へ」をクリックし、データベースの場所を選択します。デフォルトのまま使用できます。後で場所を変更することはできません。
- 「有効にする」をクリックします。
4. Firebaseの構成
Flutter で Firebase を使用するには、次のタスクを完了して、 FlutterFire
ライブラリを正しく使用するように Flutter プロジェクトを構成する必要があります。
-
FlutterFire
依存関係をプロジェクトに追加します。 - Firebase プロジェクトに目的のプラットフォームを登録します。
- プラットフォーム固有の構成ファイルをダウンロードして、コードに追加します。
Flutter アプリの最上位ディレクトリには、 android
、 ios
、 macos
、およびweb
サブディレクトリがあり、それぞれ iOS と Android のプラットフォーム固有の構成ファイルを保持します。
依存関係を構成する
このアプリで使用する 2 つの Firebase 製品 (Authentication と Firestore) のFlutterFire
ライブラリを追加する必要があります。
- コマンドラインから、次の依存関係を追加します。
$ flutter pub add firebase_core
firebase_core
パッケージは、すべての Firebase Flutter プラグインに必要な共通コードです。
$ flutter pub add firebase_auth
firebase_auth
パッケージにより、認証との統合が可能になります。
$ flutter pub add cloud_firestore
cloud_firestore
パッケージを使用すると、Firestore データ ストレージへのアクセスが可能になります。
$ flutter pub add provider
firebase_ui_auth
パッケージは、開発者の認証フローの速度を向上させるためのウィジェットとユーティリティのセットを提供します。
$ flutter pub add firebase_ui_auth
必要なパッケージを追加しましたが、Firebase を適切に使用するように iOS、Android、macOS、Web ランナー プロジェクトを構成する必要もあります。また、ビジネス ロジックを表示ロジックから分離できるprovider
パッケージも使用します。
FlutterFire CLI をインストールする
FlutterFire CLI は、基盤となる Firebase CLI に依存します。
- Firebase CLI をまだインストールしていない場合は、マシンにインストールします。
- FlutterFire CLI をインストールします。
$ dart pub global activate flutterfire_cli
flutterfire
コマンドをインストールすると、世界中で使用できるようになります。
アプリを設定する
CLI は、Firebase プロジェクトと選択したプロジェクト アプリから情報を抽出して、特定のプラットフォーム用のすべての構成を生成します。
アプリのルートで、 configure
コマンドを実行します。
$ flutterfire configure
設定コマンドは、次のプロセスを案内します。
-
.firebaserc
ファイルに基づいて、または Firebase コンソールから Firebase プロジェクトを選択します。 - Android、iOS、macOS、Web など、構成用のプラットフォームを決定します。
- 構成を抽出する Firebase アプリを特定します。デフォルトでは、CLI は現在のプロジェクト構成に基づいて Firebase アプリを自動的に照合しようとします。
- プロジェクト内に
firebase_options.dart
ファイルを生成します。
macOS を構成する
macOS 上の Flutter は、完全にサンドボックス化されたアプリを構築します。このアプリはネットワークと統合して Firebase サーバーと通信するため、ネットワーク クライアント権限を使用してアプリを構成する必要があります。
macos/Runner/DebugProfile.entitlements
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<!-- Add the following two lines -->
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
macos/Runner/Release.entitlements
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<!-- Add the following two lines -->
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
詳細については、 「Flutter のデスクトップ サポート」を参照してください。
5. RSVP 機能を追加する
Firebase をアプリに追加したので、ユーザーをAuthenticationに登録するRSVPボタンを作成できます。 Android ネイティブ、iOS ネイティブ、Web の場合は、事前に構築されたFirebaseUI Auth
パッケージがありますが、Flutter 用にこの機能を構築する必要があります。
前に取得したプロジェクトには、ほとんどの認証フローのユーザー インターフェイスを実装するウィジェットのセットが含まれていました。ビジネス ロジックを実装して、認証をアプリと統合します。
Provider
パッケージを使用してビジネス ロジックを追加する
provider
パッケージを使用して、アプリの Flutter ウィジェットのツリー全体で一元化されたアプリ状態オブジェクトを利用できるようにします。
- 次の内容を含む
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 を使用します。これは、アプリでログイン画面をすばやくブートストラップする優れた方法です。
認証フローを統合する
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';
- アプリの状態をアプリの初期化に接続し、認証フローを
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
パッケージは依存ウィジェットを再表示するタイミングを知ることができます。
-
GoRouter
構成を作成して、FirebaseUI が提供するさまざまな画面へのナビゲーションを処理できるようにアプリを更新します。
lib/main.dart
// Add GoRouter configuration outside the App class
final _router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomePage(),
routes: [
GoRoute(
path: 'sign-in',
builder: (context, state) {
return SignInScreen(
actions: [
ForgotPasswordAction(((context, email) {
final uri = Uri(
path: '/sign-in/forgot-password',
queryParameters: <String, String?>{
'email': email,
},
);
context.push(uri.toString());
})),
AuthStateChangeAction(((context, state) {
final user = switch (state) {
SignedIn state => state.user,
UserCreated state => state.credential.user,
_ => null
};
if (user == null) {
return;
}
if (state is UserCreated) {
user.updateDisplayName(user.email!.split('@')[0]);
}
if (!user.emailVerified) {
user.sendEmailVerification();
const snackBar = SnackBar(
content: Text(
'Please check your email to verify your email address'));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
context.pushReplacement('/');
})),
],
);
},
routes: [
GoRoute(
path: 'forgot-password',
builder: (context, state) {
final arguments = state.queryParameters;
return ForgotPasswordScreen(
email: arguments['email'],
headerMaxExtent: 200,
);
},
),
],
),
GoRoute(
path: 'profile',
builder: (context, state) {
return ProfileScreen(
providers: const [],
actions: [
SignedOutAction((context) {
context.pushReplacement('/');
}),
],
);
},
),
],
),
],
);
// end of GoRouter configuration
// Change MaterialApp to MaterialApp.router and add the routerConfig
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Firebase Meetup',
theme: ThemeData(
buttonTheme: Theme.of(context).buttonTheme.copyWith(
highlightColor: Colors.deepPurple,
),
primarySwatch: Colors.deepPurple,
textTheme: GoogleFonts.robotoTextTheme(
Theme.of(context).textTheme,
),
visualDensity: VisualDensity.adaptivePlatformDensity,
useMaterial3: true,
),
routerConfig: _router, // new
);
}
}
各画面には、認証フローの新しい状態に基づいて、異なるタイプのアクションが関連付けられています。認証でほとんどの状態が変化した後は、ホーム画面であっても、プロファイルなどの別の画面であっても、優先画面に再ルートできます。
-
HomePage
クラスの build メソッドで、アプリの状態をAuthFunc
ウィジェットと統合します。
lib/home_page.dart
import 'package:firebase_auth/firebase_auth.dart' // new
hide EmailAuthProvider, PhoneAuthProvider; // new
import 'package:flutter/material.dart'; // new
import 'package:provider/provider.dart'; // new
import 'app_state.dart'; // new
import 'src/authentication.dart'; // new
import 'src/widgets.dart';
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Firebase Meetup'),
),
body: ListView(
children: <Widget>[
Image.asset('assets/codelab.png'),
const SizedBox(height: 8),
const IconAndDetail(Icons.calendar_today, 'October 30'),
const IconAndDetail(Icons.location_city, 'San Francisco'),
// Add from here
Consumer<ApplicationState>(
builder: (context, appState, _) => AuthFunc(
loggedIn: appState.loggedIn,
signOut: () {
FirebaseAuth.instance.signOut();
}),
),
// to here
const Divider(
height: 8,
thickness: 1,
indent: 8,
endIndent: 8,
color: Colors.grey,
),
const Header("What we'll be doing"),
const Paragraph(
'Join us for a day full of Firebase Workshops and Pizza!',
),
],
),
);
}
}
AuthFunc
ウィジェットをインスタンス化し、それをConsumer
ウィジェットでラップします。 Consumer ウィジェットは、アプリの状態が変化したときにprovider
パッケージを使用してツリーの一部を再構築する通常の方法です。 AuthFunc
ウィジェットは、テストする補助ウィジェットです。
認証フローをテストする
- アプリで、 [RSVP]ボタンをタップして
SignInScreen
を開始します。
- メールアドレスを入力。すでに登録している場合は、パスワードの入力を求めるプロンプトが表示されます。それ以外の場合は、登録フォームに記入するよう求められます。
- エラー処理フローを確認するには、6 文字未満のパスワードを入力してください。登録されている場合は、代わりに のパスワードが表示されます。
- エラー処理フローを確認するには、間違ったパスワードを入力してください。
- 正しいパスワードを入力してください。ログイン エクスペリエンスが表示され、ユーザーはログアウトできるようになります。
6. Firestore にメッセージを書き込む
ユーザーが来ることを知るのは素晴らしいことですが、アプリ内でゲストに何か他のことを提供する必要があります。ゲストブックにメッセージを残せたらどうなるでしょうか?なぜ来ることに興奮しているのか、誰に会いたいのかを共有できます。
ユーザーがアプリに書いたチャット メッセージを保存するには、 Firestoreを使用します。
データ・モデル
Firestore は NoSQL データベースであり、データベースに保存されるデータはコレクション、ドキュメント、フィールド、サブコレクションに分割されます。チャットの各メッセージを、最上位のコレクションであるguestbook
コレクションにドキュメントとして保存します。
Firestore にメッセージを追加する
このセクションでは、ユーザーがデータベースにメッセージを書き込むための機能を追加します。まず、フォーム フィールドと送信ボタンを追加し、次にこれらの要素をデータベースに接続するコードを追加します。
-
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
があります。また、 TextFormField
がExpanded
ウィジェットでラップされているため、 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)),
ウィジェットを表示するにはこれで十分ですが、何か役立つことをするには十分ではありません。このコードを機能させるには、すぐにこのコードを更新します。
アプリのプレビュー
ユーザーがSENDをクリックすると、次のコード スニペットがトリガーされます。メッセージ入力フィールドの内容をデータベースのguestbook
コレクションに追加します。具体的には、 addMessageToGuestBook
メソッドは、 guestbook
コレクション内で自動的に生成された ID を使用して、メッセージ コンテンツを新しいドキュメントに追加します。
FirebaseAuth.instance.currentUser.uid
、認証によってログインしているすべてのユーザーに与えられる自動生成された一意の ID への参照であることに注意してください。
-
lib/app_state.dart
ファイルに、addMessageToGuestBook
メソッドを追加します。次のステップで、この機能をユーザー インターフェイスに接続します。
lib/app_state.dart
import 'package:cloud_firestore/cloud_firestore.dart'; // new
import 'package:firebase_auth/firebase_auth.dart'
hide EmailAuthProvider, PhoneAuthProvider;
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_ui_auth/firebase_ui_auth.dart';
import 'package:flutter/material.dart';
import 'firebase_options.dart';
class ApplicationState extends ChangeNotifier {
// Current content of ApplicationState elided ...
// Add from here...
Future<DocumentReference> addMessageToGuestBook(String message) {
if (!_loggedIn) {
throw Exception('Must be logged in');
}
return FirebaseFirestore.instance
.collection('guestbook')
.add(<String, dynamic>{
'text': message,
'timestamp': DateTime.now().millisecondsSinceEpoch,
'name': FirebaseAuth.instance.currentUser!.displayName,
'userId': FirebaseAuth.instance.currentUser!.uid,
});
}
// ...to here.
}
UIとデータベースを接続する
ユーザーがゲストブックに追加したいテキストを入力できる UI があり、そのエントリを Firestore に追加するコードがあります。あとは 2 つを接続するだけです。
-
lib/home_page.dart
ファイルで、HomePage
ウィジェットに次の変更を加えます。
lib/home_page.dart
import 'package:firebase_auth/firebase_auth.dart'
hide EmailAuthProvider, PhoneAuthProvider;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'app_state.dart';
import 'guest_book.dart'; // new
import 'src/authentication.dart';
import 'src/widgets.dart';
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Firebase Meetup'),
),
body: ListView(
children: <Widget>[
Image.asset('assets/codelab.png'),
const SizedBox(height: 8),
const IconAndDetail(Icons.calendar_today, 'October 30'),
const IconAndDetail(Icons.location_city, 'San Francisco'),
Consumer<ApplicationState>(
builder: (context, appState, _) => AuthFunc(
loggedIn: appState.loggedIn,
signOut: () {
FirebaseAuth.instance.signOut();
}),
),
const Divider(
height: 8,
thickness: 1,
indent: 8,
endIndent: 8,
color: Colors.grey,
),
const Header("What we'll be doing"),
const Paragraph(
'Join us for a day full of Firebase Workshops and Pizza!',
),
// Modify from here...
Consumer<ApplicationState>(
builder: (context, appState, _) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (appState.loggedIn) ...[
const Header('Discussion'),
GuestBook(
addMessage: (message) =>
appState.addMessageToGuestBook(message),
),
],
],
),
),
// ...to here.
],
),
);
}
}
このステップの最初に追加した 2 行を完全な実装に置き換えました。ここでもConsumer<ApplicationState>
を使用して、レンダリングするツリーの部分でアプリの状態を利用できるようにします。これにより、UI にメッセージを入力したユーザーに反応し、それをデータベースに公開できます。次のセクションでは、追加されたメッセージがデータベースに公開されるかどうかをテストします。
メッセージ送信のテスト
- 必要に応じて、アプリにサインインします。
-
Hey there!
などのメッセージを入力します。をクリックし、 [送信]をクリックします。
このアクションにより、メッセージが Firestore データベースに書き込まれます。ただし、データの取得を実装する必要があるため、実際の Flutter アプリにはメッセージが表示されません。これは次のステップで実行します。ただし、Firebase コンソールのデータベースダッシュボードでは、 guestbook
コレクションに追加されたメッセージを確認できます。より多くのメッセージを送信すると、 guestbook
コレクションにさらに多くのドキュメントが追加されます。たとえば、次のコード スニペットを参照してください。
7. メッセージを読む
ゲストがデータベースにメッセージを書き込めるのは素晴らしいことですが、アプリではまだ見ることができません。それを修正する時が来ました!
メッセージを同期する
メッセージを表示するには、データ変更時にトリガーするリスナーを追加し、新しいメッセージを表示する UI 要素を作成する必要があります。アプリから新しく追加されたメッセージをリッスンするコードをアプリの状態に追加します。
- 新しいファイル
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;
}
-
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
- 状態とゲッターを定義する
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.
-
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 でリアルタイム更新を取得する」を参照してください。
-
lib/guest_book.dart
ファイルに、次のインポートを追加します。
import 'guest_book_message.dart';
-
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();
}
-
_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
を生成します。
-
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 は、データベースに登録しているクライアントとデータを自動的かつ即座に同期します。
メッセージの同期をテストします。
- アプリで、データベース内で以前に作成したメッセージを見つけます。
- 新しいメッセージを書きます。それらは即座に現れます。
- 複数のウィンドウまたはタブでワークスペースを開きます。メッセージはウィンドウとタブ間でリアルタイムに同期されます。
- オプション: Firebase コンソールの[データベース]メニューで、手動でメッセージを削除、変更、または新しいメッセージを追加します。すべての変更は UI に表示されます。
おめでとう!アプリで Firestore ドキュメントを読みました。
アプリのプレビュー
8. 基本的なセキュリティ ルールを設定する
最初にテスト モードを使用するように Firestore を設定します。これは、データベースが読み取りと書き込みのために開いていることを意味します。ただし、テスト モードは開発の初期段階でのみ使用してください。ベスト プラクティスとして、アプリの開発時にデータベースのセキュリティ ルールを設定する必要があります。セキュリティはアプリの構造と動作に不可欠です。
Firebase セキュリティ ルールを使用すると、データベース内のドキュメントやコレクションへのアクセスを制御できます。柔軟なルール構文を使用すると、データベース全体へのすべての書き込みから特定のドキュメントの操作まで、あらゆるものに一致するルールを作成できます。
基本的なセキュリティ ルールを設定します。
- Firebase コンソールの[開発]メニューで、 [データベース] > [ルール]をクリックします。次のデフォルトのセキュリティ ルールと、ルールが公開されているという警告が表示されます。
- アプリがデータを書き込むコレクションを特定します。
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/app_state.dart
ファイルで、ApplicationState
の accessors セクションに次の行を追加して、UI コードがこの状態と対話できるようにします。
lib/app_state.dart
int _attendees = 0;
int get attendees => _attendees;
Attending _attending = Attending.unknown;
StreamSubscription<DocumentSnapshot>? _attendingSubscription;
Attending get attending => _attending;
set attending(Attending attending) {
final userDoc = FirebaseFirestore.instance
.collection('attendees')
.doc(FirebaseAuth.instance.currentUser!.uid);
if (attending == Attending.yes) {
userDoc.set(<String, dynamic>{'attending': true});
} else {
userDoc.set(<String, dynamic>{'attending': false});
}
}
-
ApplicationState
のinit()
メソッドを次のように更新します。
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 番目のクエリを追加します。
-
lib/app_state.dart
ファイルの先頭に次の列挙を追加します。
lib/app_state.dart
enum Attending { yes, no, unknown }
- 新しいファイル
yes_no_selection.dart
を作成し、ラジオ ボタンのように機能する新しいウィジェットを定義します。
lib/yes_no_selection.dart
import 'package:flutter/material.dart';
import 'app_state.dart';
import 'src/widgets.dart';
class YesNoSelection extends StatelessWidget {
const YesNoSelection(
{super.key, required this.state, required this.onSelection});
final Attending state;
final void Function(Attending selection) onSelection;
@override
Widget build(BuildContext context) {
switch (state) {
case Attending.yes:
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
FilledButton(
onPressed: () => onSelection(Attending.yes),
child: const Text('YES'),
),
const SizedBox(width: 8),
TextButton(
onPressed: () => onSelection(Attending.no),
child: const Text('NO'),
),
],
),
);
case Attending.no:
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
TextButton(
onPressed: () => onSelection(Attending.yes),
child: const Text('YES'),
),
const SizedBox(width: 8),
FilledButton(
onPressed: () => onSelection(Attending.no),
child: const Text('NO'),
),
],
),
);
default:
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
StyledButton(
onPressed: () => onSelection(Attending.yes),
child: const Text('YES'),
),
const SizedBox(width: 8),
StyledButton(
onPressed: () => onSelection(Attending.no),
child: const Text('NO'),
),
],
),
);
}
}
}
「はい」も「いいえ」も選択されていない不定状態で開始されます。ユーザーが出席するかどうかを選択すると、そのオプションが塗りつぶされたボタンで強調表示され、他のオプションはフラットなレンダリングで隠れます。
-
HomePage
のbuild()
メソッドを更新してYesNoSelection
を利用し、ログイン ユーザーが参加するかどうかを指定できるようにし、イベントの参加者数を表示できるようにします。
lib/home_page.dart
Consumer<ApplicationState>(
builder: (context, appState, _) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Add from here...
switch (appState.attendees) {
1 => const Paragraph('1 person going'),
>= 2 => Paragraph('${appState.attendees} people going'),
_ => const Paragraph('No one going'),
},
// ...to here.
if (appState.loggedIn) ...[
// Add from here...
YesNoSelection(
state: appState.attending,
onSelection: (attending) => appState.attending = attending,
),
// ...to here.
const Header('Discussion'),
GuestBook(
addMessage: (message) =>
appState.addMessageToGuestBook(message),
messages: appState.guestBookMessages,
),
],
],
),
),
ルールの追加
すでにいくつかのルールが設定されているため、ボタンを使用して追加したデータは拒否されます。 attendees
コレクションへの追加を許可するには、ルールを更新する必要があります。
-
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 コンソールの Firestore ダッシュボードに結果が表示されます。
アプリのプレビュー
10. おめでとうございます!
Firebase を使用して、インタラクティブなリアルタイム Web アプリを構築しました。