1. 始める前に
この Codelab では、Android および iOS 向けの Flutter モバイルアプリを作成するためのFirebaseの基本をいくつか学びます。
前提条件
- Flutter に慣れる
- Flutter SDK
- お好きなテキストエディタ
学習内容
- Flutter を使用して、Android、iOS、Web、macOS でイベント出欠確認とゲストブック チャット アプリを構築する方法。
- 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
ディレクトリには、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
ディレクトリを開くかインポートします。このディレクトリには、まだ機能していない Flutter ミートアップ アプリで構成される Codelab のスターター コードが含まれています。
作業が必要なファイルを見つける
このアプリのコードは、複数のディレクトリに分散しています。このように機能を分割すると、機能ごとにコードがグループ化されるため、作業が容易になります。
- 次のファイルを見つけます。
-
lib/main.dart
: このファイルには、メイン エントリ ポイントとアプリ ウィジェットが含まれています。 -
lib/home_page.dart
: このファイルには、ホームページ ウィジェットが含まれています。 -
lib/src/widgets.dart
: このファイルには、アプリのスタイルを標準化するのに役立ついくつかのウィジェットが含まれています。これらは、スターター アプリの画面を構成します。 -
lib/src/authentication.dart
: このファイルには、Firebase メールベース認証のログイン ユーザー エクスペリエンスを作成するための一連のウィジェットを使用したAuthenticationの部分的な実装が含まれています。認証フロー用のこれらのウィジェットは、スターター アプリではまだ使用されていませんが、すぐに追加します。
-
アプリの残りの部分をビルドするには、必要に応じてファイルを追加します。
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 アナリティクスの設定はスキップしてください。
Firebase プロジェクトの詳細については、 Firebase プロジェクトについて を参照してください。
このアプリは、ウェブアプリで利用できる次の Firebase プロダクトを使用します。
- 認証:ユーザーがアプリにサインインできるようにします。
- Firestore:構造化データをクラウドに保存し、データが変更されたときに即座に通知を受け取ります。
- Firebase セキュリティ ルール:データベースを保護します。
これらの製品の中には、特別な構成が必要なものや、Firebase コンソールで有効にする必要があるものがあります。
メールサインイン認証を有効にする
- Firebase コンソールの[プロジェクトの概要]ペインで、 [ビルド]メニューを展開します。
- Authentication > Get Started > Sign-in method > Email/Password > Enable > Save をクリックします。
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
パッケージにより、 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、および 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 用にこの機能をビルドする必要があります。
以前に取得したプロジェクトには、ほとんどの認証フローのユーザー インターフェイスを実装する一連のウィジェットが含まれていました。ビジネス ロジックを実装して、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) {
if (state is SignedIn || state is UserCreated) {
var user = (state is SignedIn)
? state.user
: (state as UserCreated).credential.user;
if (user == null) {
return;
}
if (state is UserCreated) {
user.updateDisplayName(user.email!.split('@')[0]);
}
if (!user.emailVerified) {
user.sendEmailVerification();
const snackBar = SnackBar(
content: Text(
'Please check your email to verify your email address'));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
context.pushReplacement('/');
}
})),
],
);
},
routes: [
GoRoute(
path: 'forgot-password',
builder: (context, state) {
final arguments = state.queryParams;
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 データベースであり、データベースに格納されたデータはコレクション、ドキュメント、フィールド、およびサブコレクションに分割されます。チャットの各メッセージは、最上位のコレクションであるgustbook
コレクションにドキュメントとして保存します。
メッセージを 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
、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 コンソールのデータベースダッシュボードでは、追加したメッセージが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. ボーナスステップ: 学んだことを実践する
出席者の RSVP ステータスを記録する
現在、あなたのアプリでは、ユーザーがイベントに興味を持っている場合にのみチャットを許可しています。また、誰かが来るかどうかを知る唯一の方法は、チャットでそう言ったときです。
このステップでは、組織化して、何人の人が来るかを人々に知らせます。アプリの状態にいくつかの機能を追加します。 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) {
_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: [
ElevatedButton(
style: ElevatedButton.styleFrom(elevation: 0),
onPressed: () => onSelection(Attending.yes),
child: const Text('YES'),
),
const SizedBox(width: 8),
TextButton(
onPressed: () => onSelection(Attending.no),
child: const Text('NO'),
),
],
),
);
case Attending.no:
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
TextButton(
onPressed: () => onSelection(Attending.yes),
child: const Text('YES'),
),
const SizedBox(width: 8),
ElevatedButton(
style: ElevatedButton.styleFrom(elevation: 0),
onPressed: () => onSelection(Attending.no),
child: const Text('NO'),
),
],
),
);
default:
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
StyledButton(
onPressed: () => onSelection(Attending.yes),
child: const Text('YES'),
),
const SizedBox(width: 8),
StyledButton(
onPressed: () => onSelection(Attending.no),
child: const Text('NO'),
),
],
),
);
}
}
}
はいもいいえも選択されていない不確定な状態で開始されます。ユーザーが出席するかどうかを選択すると、そのオプションが塗りつぶされたボタンで強調表示され、他のオプションはフラット レンダリングで後退します。
-
HomePage
のbuild()
メソッドを更新してYesNoSelection
を利用し、ログイン ユーザーが出席するかどうかを指定できるようにし、イベントの出席者数を表示します。
lib/home_page.dart
Consumer<ApplicationState>(
builder: (context, appState, _) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Add from here...
if (appState.attendees >= 2)
Paragraph('${appState.attendees} people going')
else if (appState.attendees == 1)
const Paragraph('1 person going')
else
const Paragraph('No one going'),
// ...to here.
if (appState.loggedIn) ...[
// Add from here...
YesNoSelection(
state: appState.attending,
onSelection: (attending) => appState.attending = attending,
),
// ...to here.
const Header('Discussion'),
GuestBook(
addMessage: (message) =>
appState.addMessageToGuestBook(message),
messages: appState.guestBookMessages,
),
],
],
),
),
ルールを追加
すでにいくつかのルールを設定しているため、ボタンで追加したデータは拒否されます。 attendees
コレクションへの追加を許可するには、ルールを更新する必要があります。
-
attendees
コレクションで、ドキュメント名として使用した認証 UID を取得し、送信者のuid
が作成中のドキュメントと同じであることを確認します。
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// ... //
match /attendees/{userId} {
allow read: if true;
allow write: if request.auth.uid == userId;
}
}
}
出席者リストにはプライベート データがないため、誰もが出席者リストを読むことができますが、更新できるのは作成者だけです。
- データ検証を追加して、予期されるすべてのフィールドがドキュメントに存在することを確認します。
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// ... //
match /attendees/{userId} {
allow read: if true;
allow write: if request.auth.uid == userId
&& "attending" in request.resource.data;
}
}
}
- オプション: アプリでボタンをクリックして、Firebase コンソールの Firestore ダッシュボードに結果を表示します。
アプリのプレビュー
10.おめでとう!
Firebase を使用して、インタラクティブなリアルタイム ウェブアプリを構築しました。