1. 始める前に
この Codelab では、Firebase の基本を学び、Android と iOS 用の Flutter モバイルアプリを作成します。
前提条件
- Flutter に関する知識
- Flutter SDK
- 任意のテキスト エディタ
学習内容
- Android、iOS、ウェブ、macOS で、Flutter を使用してイベントの出欠確認とゲストブックのチャットアプリを作成する方法。
- Firebase Authentication でユーザーを認証し、Firestore でデータを同期する。
必要なもの
次のいずれかのデバイス:
- パソコンに接続され、デベロッパー モードに設定された物理デバイス(Android または iOS)
- iOS シミュレータ(Xcode ツールが必要)
- Android Emulator(Android Studio でのセットアップが必要)
また、次のものも必要です。
- 任意のブラウザ(Google Chrome など)。
- Dart プラグインと Flutter プラグインで構成された任意の IDE またはテキスト エディタ(Android Studio、Visual Studio Code など)。
- 最新の
stable
バージョンの Flutter またはbeta
(エッジで作業している場合)。 - Firebase プロジェクトの作成と管理に使用する Google アカウント。
- Google アカウントにログインした
Firebase
CLI。
2. サンプルコードを取得する
GitHub からプロジェクトの初期バージョンをダウンロードします。
- コマンドラインから、
flutter-codelabs
ディレクトリに GitHub リポジトリのクローンを作成します。
git clone https://github.com/flutter/codelabs.git flutter-codelabs
flutter-codelabs
ディレクトリには、Codelab のコレクションのコードが含まれています。この Codelab のコードは flutter-codelabs/firebase-get-to-know-flutter
ディレクトリにあります。このディレクトリには、各ステップの終了時にプロジェクトがどのようになるかを示した一連のスナップショットが含まれています。たとえば、2 番目のステップにいる場合。
- 2 番目のステップで一致するファイルを探します。
cd flutter-codelabs/firebase-get-to-know-flutter/step_02
ステップをスキップする場合や、あるステップの後に何かがどう表示されるかを確認するには、対象のステップにちなんだ名前のディレクトリを確認します。
スターター アプリをインポートする
- お好みの IDE で、
flutter-codelabs/firebase-get-to-know-flutter/step_02
ディレクトリを開くかインポートします。このディレクトリには、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 アナリティクスを使用する予定がないため、Google アナリティクスの設定はスキップします。
Firebase プロジェクトの詳細については、Firebase プロジェクトについて理解するをご覧ください。
このアプリでは、ウェブアプリで利用可能な次の Firebase プロダクトを使用します。
- 認証: ユーザーがアプリにログインできるようにします。
- Firestore: 構造化データをクラウドに保存し、データが変更されるとすぐに通知を受け取れます。
- Firebase セキュリティ ルール: データベースを保護します。
これらのプロダクトの一部は、特別な構成を必要とするか、Firebase コンソールで有効にする必要があります。
メールのログイン認証を有効にする
- Firebase コンソールの [プロジェクトの概要] ペインで、[ビルド] メニューを開きます。
- [Authentication] > [Get Started] > [Sign-in method] > [Email/Password] > [Enable] > [Save] をクリックします。
Firestore を有効にする
このウェブアプリは、Firestore を使用してチャット メッセージの保存と新しいチャット メッセージの受信を行います。
Firestore を有効にします。
- [構築] メニューで、[Firestore Database >データベースを作成する。
- [テストモードで開始] を選択し、セキュリティ ルールに関する免責条項を確認します。テストモードでは、開発中にデータベースに自由に書き込めます。
- [次へ] をクリックし、データベースのロケーションを選択します。デフォルトを使用できます。ロケーションは後で変更できません。
- [有効にする] をクリックします。
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
パッケージを使用すると、Authentication との統合が可能になります。
$ flutter pub add cloud_firestore
cloud_firestore
パッケージを使用すると、Firestore データ ストレージにアクセスできます。
$ flutter pub add provider
firebase_ui_auth
パッケージには、認証フローで開発速度を上げるためのウィジェットとユーティリティのセットが用意されています。
$ flutter pub add firebase_ui_auth
必要なパッケージを追加しましたが、Firebase を適切に使用できるように、iOS、Android、macOS、ウェブのランナー プロジェクトを構成する必要があります。また、ビジネス ロジックとディスプレイ ロジックを分離できる 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 の場合はこの機能をビルドする必要があります。
前に取得したプロジェクトには、認証フローのほとんどのユーザー インターフェースを実装する一連のウィジェットが含まれています。Authentication をアプリと統合するためのビジネス ロジックを実装します。
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
クラスのビルドメソッドで、アプリの状態を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 コンソールの [データベース] メニューで、メッセージを手動で削除、変更、追加します。すべての変更が 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
ファイルで、ApplicationState
のアクセサ セクションに次の行を追加して、UI コードがこの状態とやり取りできるようにします。
lib/app_state.dart
int _attendees = 0;
int get attendees => _attendees;
Attending _attending = Attending.unknown;
StreamSubscription<DocumentSnapshot>? _attendingSubscription;
Attending get attending => _attending;
set attending(Attending attending) {
final userDoc = FirebaseFirestore.instance
.collection('attendees')
.doc(FirebaseAuth.instance.currentUser!.uid);
if (attending == Attending.yes) {
userDoc.set(<String, dynamic>{'attending': true});
} else {
userDoc.set(<String, dynamic>{'attending': false});
}
}
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'),
),
],
),
);
}
}
}
[Yes] も [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
コレクションで、ドキュメント名として使用した 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 を使用して、インタラクティブなリアルタイム ウェブアプリを作成しました。