1. 始める前に
この Codelab では、Firebase の基本を学び、Android と iOS 用の Flutter モバイルアプリを作成します。
前提条件
- Flutter に関する知識
- Flutter SDK
- 任意のテキスト エディタ
学習内容
- Flutter を使用して、Android、iOS、ウェブ、macOS でイベントの RSVP とゲストブックのチャットアプリを作成する方法。
- Firebase Authentication でユーザーを認証し、Firestore とデータを同期する方法。
必要なもの
次のいずれかのデバイス:
- パソコンに接続され、デベロッパー モードに設定された物理デバイス(Android または iOS)
- iOS シミュレータ(Xcode ツールが必要)
- Android Emulator(Android Studio でセットアップが必要)。
また、次のものも必要です。
- 任意のブラウザ(Google Chrome など)。
- Dart プラグインと Flutter プラグインで構成された任意の IDE またはテキスト エディタ(Android Studio、Visual Studio Code など)。
- Flutter の最新の
stable
バージョン、またはエッジデベロッパーの場合はbeta
。 - Firebase プロジェクトの作成と管理に使用する Google アカウント。
- Google アカウントにログインした
Firebase
CLI。
2. サンプルコードを取得する
GitHub からプロジェクトの初期バージョンをダウンロードする:
- コマンドラインから、GitHub リポジトリのクローンを
flutter-codelabs
ディレクトリに作成します。
git clone https://github.com/flutter/codelabs.git flutter-codelabs
flutter-codelabs
ディレクトリには、Codelab のコレクションのコードが含まれています。この Codelab のコードは flutter-codelabs/firebase-get-to-know-flutter
ディレクトリにあります。このディレクトリには、各ステップの終了時にプロジェクトがどのようになるかを示した一連のスナップショットが含まれています。たとえば、2 番目のステップに進んでいる場合。
- 2 番目のステップに一致するファイルを探します。
cd flutter-codelabs/firebase-get-to-know-flutter/step_02
先に進む場合や、手順後の状態を確認したい場合は、該当する手順の名前が付けられたディレクトリを参照してください。
スターター アプリをインポートする
flutter-codelabs/firebase-get-to-know-flutter/step_02
ディレクトリを開くか、お好みの IDE にインポートします。このディレクトリには、Codelab のスターター コードが格納されています。コードは、まだ機能しない Flutter の Meetup アプリで構成されています。
修正が必要なファイルを探す
このアプリのコードは複数のディレクトリに分散されています。このように機能を分割すると、コードが機能別にグループ化されるため、作業が容易になります。
- 次のファイルを探します。
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、ウェブ、macOS でのアプリの表示は次のようになります。
3. Firebase プロジェクトを作成して構成する
イベント情報の表示はゲストにとって便利ですが、それだけではあまり役に立ちません。アプリに動的機能を追加する必要があります。そのためには、Firebase をアプリに接続する必要があります。Firebase の利用を開始するには、Firebase プロジェクトを作成して構成する必要があります。
Firebase プロジェクトを作成する
- Firebase にログインします。
- コンソールで、[プロジェクトを追加] または [プロジェクトを作成] をクリックします。
- [プロジェクト名] フィールドに「Firebase-Flutter-Codelab」と入力し、[続行] をクリックします。
- プロジェクト作成オプションをクリックします。プロンプトが表示されたら、Firebase の利用規約に同意します。ただし、このアプリでは Google アナリティクスを使用しないため、設定はスキップします。
Firebase プロジェクトの詳細については、Firebase プロジェクトについて理解するをご覧ください。
このアプリでは、ウェブアプリで利用可能な次の Firebase プロダクトを使用します。
- 認証: ユーザーがアプリにログインできるようにします。
- Firestore: 構造化データをクラウドに保存し、データが変更されたときに即座に通知を受け取ります。
- Firebase セキュリティ ルール: データベースを保護します。
この中には、特別な設定が必要になるプロダクトや、Firebase コンソールで有効にする必要があるプロダクトがあります。
メールによるログイン認証を有効にする
- Firebase コンソールの [プロジェクトの概要] ペインで、[ビルド] メニューを開きます。
- [Authentication] > [Get Started] > [Sign-in method] > [Email/Password] > [Enable] > [Save] をクリックします。
Firestore の設定
このウェブアプリは Firestore を使用してチャット メッセージを保存し、新しいチャット メッセージを受信します。
Firebase プロジェクトに Firestore を設定する方法は次のとおりです。
- Firebase コンソールの左側のパネルで [Build] を開き、[Firestore データベース] を選択します。
- [データベースを作成] をクリックします。
- [データベース ID] は
(default)
のままにします。 - データベースのロケーションを選択し、[次へ] をクリックします。
実際のアプリの場合は、ユーザーに近いロケーションを選択します。 - [テストモードで開始] をクリックします。セキュリティ ルールに関する免責条項を確認します。
この Codelab の後半で、セキュリティ ルールを追加してデータを保護します。データベースのセキュリティ ルールを追加せずに、アプリを配布または公開しないでください。 - [作成] をクリックします。
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、ウェブのランナー プロジェクトを構成する必要があります。また、ビジネス ロジックとディスプレイ ロジックを分離できる 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、ウェブなど)を決定します。
- 構成を抽出する 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. 出欠確認機能を追加する
Firebase をアプリに追加したので、認証でユーザーを登録する [RSVP] ボタンを作成できます。Android ネイティブ、iOS ネイティブ、ウェブには、事前ビルドされた 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.uri.queryParameters;
return ForgotPasswordScreen(
email: arguments['email'],
headerMaxExtent: 200,
);
},
),
],
),
GoRoute(
path: 'profile',
builder: (context, state) {
return ProfileScreen(
providers: const [],
actions: [
SignedOutAction((context) {
context.pushReplacement('/');
}),
],
);
},
),
],
),
],
);
// end of GoRouter configuration
// Change MaterialApp to MaterialApp.router and add the routerConfig
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Firebase Meetup',
theme: ThemeData(
buttonTheme: Theme.of(context).buttonTheme.copyWith(
highlightColor: Colors.deepPurple,
),
primarySwatch: Colors.deepPurple,
textTheme: GoogleFonts.robotoTextTheme(
Theme.of(context).textTheme,
),
visualDensity: VisualDensity.adaptivePlatformDensity,
useMaterial3: true,
),
routerConfig: _router, // new
);
}
}
各画面には、認証フローの新しい状態に基づいて、関連付けられたアクションの種類が異なります。認証の状態がほとんど変化した後、ホーム画面やプロフィールなどの任意の画面にリダイレクトできます。
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
ウィジェットでラップします。コンシューマ ウィジェットは、アプリの状態が変化したときに 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
を使用してフォームの背後にあるフォームの状態にアクセスします。鍵とその使用方法の詳細については、鍵を使用するタイミングをご覧ください。
また、ウィジェットのレイアウト方法にも注目してください。Row
には TextFormField
と StyledButton
があり、StyledButton
には Row
が含まれています。また、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)),
これはウィジェットを表示するには十分ですが、有用な処理を行うには不十分です。このコードは、後で機能するように更新します。
アプリのプレビュー
ユーザーが [送信] をクリックすると、次のコード スニペットがトリガーされます。メッセージ入力フィールドの内容がデータベースの guestbook
コレクションに追加されます。具体的には、addMessageToGuestBook
メソッドは、guestbook
コレクション内の自動生成された ID を持つ新しいドキュメントにメッセージ コンテンツを追加します。
FirebaseAuth.instance.currentUser.uid
は、ログインしたすべてのユーザーに Authentication が付与する自動生成の一意の ID への参照です。
lib/app_state.dart
ファイルにaddMessageToGuestBook
メソッドを追加します。この機能は、次のステップでユーザー インターフェースに接続します。
lib/app_state.dart
import 'package:cloud_firestore/cloud_firestore.dart'; // new
import 'package:firebase_auth/firebase_auth.dart'
hide EmailAuthProvider, PhoneAuthProvider;
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_ui_auth/firebase_ui_auth.dart';
import 'package:flutter/material.dart';
import 'firebase_options.dart';
class ApplicationState extends ChangeNotifier {
// Current content of ApplicationState elided ...
// Add from here...
Future<DocumentReference> addMessageToGuestBook(String message) {
if (!_loggedIn) {
throw Exception('Must be logged in');
}
return FirebaseFirestore.instance
.collection('guestbook')
.add(<String, dynamic>{
'text': message,
'timestamp': DateTime.now().millisecondsSinceEpoch,
'name': FirebaseAuth.instance.currentUser!.displayName,
'userId': FirebaseAuth.instance.currentUser!.uid,
});
}
// ...to here.
}
UI とデータベースを接続する
ユーザーがゲストブックに追加するテキストを入力できる UI と、Firestore にエントリを追加するコードができました。後は、2 つを接続するだけです。
lib/home_page.dart
ファイルで、HomePage
ウィジェットに次のように変更を加えます。
lib/home_page.dart
import 'package:firebase_auth/firebase_auth.dart'
hide EmailAuthProvider, PhoneAuthProvider;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'app_state.dart';
import 'guest_book.dart'; // new
import 'src/authentication.dart';
import 'src/widgets.dart';
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Firebase Meetup'),
),
body: ListView(
children: <Widget>[
Image.asset('assets/codelab.png'),
const SizedBox(height: 8),
const IconAndDetail(Icons.calendar_today, 'October 30'),
const IconAndDetail(Icons.location_city, 'San Francisco'),
Consumer<ApplicationState>(
builder: (context, appState, _) => AuthFunc(
loggedIn: appState.loggedIn,
signOut: () {
FirebaseAuth.instance.signOut();
}),
),
const Divider(
height: 8,
thickness: 1,
indent: 8,
endIndent: 8,
color: Colors.grey,
),
const Header("What we'll be doing"),
const Paragraph(
'Join us for a day full of Firebase Workshops and Pizza!',
),
// Modify from here...
Consumer<ApplicationState>(
builder: (context, appState, _) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (appState.loggedIn) ...[
const Header('Discussion'),
GuestBook(
addMessage: (message) =>
appState.addMessageToGuestBook(message),
),
],
],
),
),
// ...to here.
],
),
);
}
}
このステップの始めに追加した 2 行を、完全な実装に置き換えました。ここでも Consumer<ApplicationState>
を使用して、レンダリングするツリーの部分でアプリの状態を利用できるようにします。これにより、UI でメッセージを入力したユーザーにリアクションし、データベースに公開できます。次のセクションでは、追加したメッセージがデータベースにパブリッシュされているかどうかをテストします。
メッセージの送信をテストする
- 必要に応じて、アプリにログインします。
Hey there!
などのメッセージを入力し、[送信] をクリックします。
このアクションにより、メッセージが Firestore データベースに書き込まれます。ただし、データの取得が実装されていないため、実際の Flutter アプリにはまだメッセージが表示されません。これは次のステップで対応します。ただし、Firebase コンソールの Database ダッシュボードでは、追加したメッセージが 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 を追加します。
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
を生成します。
- 新しい
messages
パラメータを使用してGuestBook
を正しく作成するように、HomePage
の本文を更新します。
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 コンソールの [Database] メニューで、メッセージを手動で削除、変更、または追加します。すべての変更が 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.
}
}
各ゲストブック ドキュメントで Authentication UID をフィールドとして使用したため、Authentication UID を取得して、ドキュメントへの書き込みを試みるすべてのユーザーに一致する Authentication 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
ファイルで、UI コードがこの状態を操作できるように、ApplicationState
の accessors セクションに次の行を追加します。
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) {
_loggedIn = true;
_emailVerified = user.emailVerified;
_guestBookSubscription = FirebaseFirestore.instance
.collection('guestbook')
.orderBy('timestamp', descending: true)
.snapshots()
.listen((snapshot) {
_guestBookMessages = [];
for (final document in snapshot.docs) {
_guestBookMessages.add(
GuestBookMessage(
name: document.data()['name'] as String,
message: document.data()['text'] as String,
),
);
}
notifyListeners();
});
// Add from here...
_attendingSubscription = FirebaseFirestore.instance
.collection('attendees')
.doc(user.uid)
.snapshots()
.listen((snapshot) {
if (snapshot.data() != null) {
if (snapshot.data()!['attending'] as bool) {
_attending = Attending.yes;
} else {
_attending = Attending.no;
}
} else {
_attending = Attending.unknown;
}
notifyListeners();
});
// ...to here.
} else {
_loggedIn = false;
_emailVerified = false;
_guestBookMessages = [];
_guestBookSubscription?.cancel();
_attendingSubscription?.cancel(); // new
}
notifyListeners();
});
}
このコードは、参加者数を特定するために常に定期購入されるクエリと、ユーザーがログインしている間のみアクティブで、ユーザーが参加しているかどうかを特定する 2 つ目のクエリを追加します。
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'),
),
],
),
);
}
}
}
最初は [はい] も [いいえ] も選択されていない未確定の状態です。ユーザーが参加するかどうかを選択すると、そのオプションがハイライト表示された塗りつぶしボタンで表示され、他のオプションはフラット レンダリングで表示されなくなります。
YesNoSelection
を活用するようにHomePage
のbuild()
メソッドを更新し、ログインしたユーザーが参加するかどうかを指定できるようにして、イベントの参加者数を表示します。
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
コレクションで、ドキュメント名として使用した Authentication 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 を使用して、インタラクティブなリアルタイム ウェブアプリを作成しました。