FlutterFireを勉強するスレ
FirebaseCLIをインストール
brew install firebase-cli
Firebase CLIが使えるかテスト
firebase login
コマンドを入力すると
- Firebaseのエラーレポートの報告の有無の確認
- ブラウザが起動され、Googleアカウントの選択
- 成功したら、「Success! Logged in as {Googleアカウント名}」と表示される
アカウントに正常にログインできるかテストする
firebase projects:list
CodeLabsのサンプル
git clone https://github.com/flutter/codelabs.git flutter-codelabs
-
flutter-codelabs/firebase-get-to-know-flutter/step_02
を開く - lib/main.dart : メインのエントリポイントとアプリケーションwidgetが含まれている
- lib/src/widgets.dart : Appのスタイル標準化に役立つwidgetが含まれる。また、スターターアプリの画面を構成するために使用される。
- lib/src/authentication.dart : Firebaseのメールベース認証の処理が含まれる
Firebaseのプロジェクトを作成する
ログインし、プロジェクトを作成する
Firebase認証のメールログインを有効化
- 左のタブから構築 -> Authentication
- 上に表示されている「始める」をクリック
- Sign-in method -> 「メール/パスワード」⇨ 有効にする ⇨ 保存
CloudFirestoreを有効化
Webアプリは、CloudFirestoreを使用しチャットメッセージを保存し、新しいチャットメッセージを受信する。
- 構築 ⇨ Firestore Database -> 「データベースの作成」をクリック
- 「テストモードで開始する」を選択
- 「データベスの作成」項目では、データベースの場所を選択する。この場所は後で変更できない。
https://firebase.google.com/docs/firestore/locations?hl=ja
基本的には、「asia-northeast1」(東京)を選択すればOK
Firebaseの構成
FlutterでFirebaseを使用するためには、FlutterFireライブラリを正しく利用するための設定が必要
- FlutterFireの依存関係をプロジェクトに追加する
- 目的のプラットフォームをFirebaseプロジェクトに登録
- プラットフォーム固有の構成ファイルをDL&コードに追加
依存関係構築
// firebase_core : FirebaseFlutterプラグインに必要な共通コード
flutter pub add firebase_core
// firebase_auth : Firebaseの認証機能
flutter pub add firebase_auth
// cloud_firestore : CloudFirestoreデータすレージへのアクセスを有効化
flutter pub add cloud_firestore
// ビジネスロジックを表示ロジックから分離するために利用
flutter pub add provider
flutterfireをインストール
FlutterFire CLIは、基盤となるFirebaseCLIに依存する。
dart pub global activate flutterfire_cli
// Flutter SDKを保存しているディレクトリに移動し、パスを通す
export PATH="$PATH":"$HOME/.pub-cache/bin"
アプリの構成
iOS,Android,Webの構成を自動生成してくれる
flutterfire configure
実行すると
- Firebaseプロジェクトの選択
- 構成するプラットフォームの選択
- 選択したプラットフォーム構成を抽出。デフォルトは、現在のプロジェクト構成に基づき自動的に照合させる。
- プロジェクトでfirebase_options.dartファイルを生成する
コマンド実行後の状態
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: step_02/android/app/build.gradle
modified: step_02/android/build.gradle
modified: step_02/pubspec.lock
modified: step_02/pubspec.yaml
Untracked files:
(use "git add <file>..." to include in what will be committed)
step_02/android/app/google-services.json
step_02/ios/firebase_app_id_file.json
step_02/lib/firebase_options.dart
step_02/macos/firebase_app_id_file.json
git status
コマンドより、
編集されたファイル
- android/app/build.gradle
- android/build.gradle
自動生成されたファイル
- android/app/google-services.json
- ios/firebase_app_id_file.json
- lib/firebase_options.dart
- macos/firebase_app_id_file.json
Firebaseコンソールからも3種類のアプリケーションが追加されたことが確認できる。
macOSを構成する
<?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>
<?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>
ユーザーサインインの追加
main.dartの編集
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
import 'firebase_options.dart';
import 'src/authentication.dart';
import 'src/widgets.dart';
class ApplicationState extends ChangeNotifier {
ApplicationState() {
init();
}
Future<void> init() async {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
FirebaseAuth.instance.userChanges().listen((user) {
if (user != null) {
_loginState = ApplicationLoginState.loggedIn;
} else {
_loginState = ApplicationLoginState.loggedOut;
}
notifyListeners();
});
}
ApplicationLoginState _loginState = ApplicationLoginState.loggedOut;
ApplicationLoginState get loginState => _loginState;
String? _email;
String? get email => _email;
void startLoginFlow() {
_loginState = ApplicationLoginState.emailAddress;
notifyListeners();
}
Future<void> verifyEmail(
String email,
void Function(FirebaseAuthException e) errorCallback,
) async {
try {
var methods =
await FirebaseAuth.instance.fetchSignInMethodsForEmail(email);
if (methods.contains('password')) {
_loginState = ApplicationLoginState.password;
} else {
_loginState = ApplicationLoginState.register;
}
_email = email;
notifyListeners();
} on FirebaseAuthException catch (e) {
errorCallback(e);
}
}
Future<void> signInWithEmailAndPassword(
String email,
String password,
void Function(FirebaseAuthException e) errorCallback,
) async {
try {
await FirebaseAuth.instance.signInWithEmailAndPassword(
email: email,
password: password,
);
} on FirebaseAuthException catch (e) {
errorCallback(e);
}
}
void cancelRegistration() {
_loginState = ApplicationLoginState.emailAddress;
notifyListeners();
}
Future<void> registerAccount(
String email,
String displayName,
String password,
void Function(FirebaseAuthException e) errorCallback) async {
try {
var credential = await FirebaseAuth.instance
.createUserWithEmailAndPassword(email: email, password: password);
await credential.user!.updateDisplayName(displayName);
} on FirebaseAuthException catch (e) {
errorCallback(e);
}
}
void signOut() {
FirebaseAuth.instance.signOut();
}
}
void main() {
// Modify from here
runApp(
ChangeNotifierProvider(
create: (context) => ApplicationState(),
builder: (context, _) => App(),
),
);
// to here.
}
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
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,
),
home: const HomePage(),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Firebase Meetup'),
),
body: ListView(
children: <Widget>[
Image.asset('assets/codelab.png'),
const SizedBox(height: 8),
const IconAndDetail(Icons.calendar_today, 'October 30'),
const IconAndDetail(Icons.location_city, 'San Francisco'),
// Add from here
Consumer<ApplicationState>(
builder: (context, appState, _) => Authentication(
email: appState.email,
loginState: appState.loginState,
startLoginFlow: appState.startLoginFlow,
verifyEmail: appState.verifyEmail,
signInWithEmailAndPassword: appState.signInWithEmailAndPassword,
cancelRegistration: appState.cancelRegistration,
registerAccount: appState.registerAccount,
signOut: appState.signOut,
),
),
// to here
const Divider(
height: 8,
thickness: 1,
indent: 8,
endIndent: 8,
color: Colors.grey,
),
const Header("What we'll be doing"),
const Paragraph(
'Join us for a day full of Firebase Workshops and Pizza!',
),
],
),
);
}
}
ビルドを通す
could not find included file ‘Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig’ in search paths
パッケージを追加し、main.dartを編集してがエラーを吐く。この編集をするためには、
- Podfileを編集する
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
'Debug-development' => :debug,
'Profile-development' => :release,
'Release-development' => :release,
'Debug-production' => :debug,
'Profile-production' => :release,
'Release-production' => :release,
}
- iosのファオルダに移動し、
pod install
flutter clean
flutter pub get
- Xcodeを再起動&実行
iOSのビルド時間を高速化
Firebaseを導入した7-8分ほどビルドに要した。
上記の記事曰く、
- Firestore iOS SDKがC++の50万行でできている
- ビルド時、Flutterのプログラム&Firebaseのプログラムをビルドしている
- firestore-ios-sdk-frameworksを導入する
- 「firestore-ios-sdk-frameworks」は、プリコンパイルされたやつ
Podfileを編集
target 'Runner' do
use_frameworks!
use_modular_headers!
pod 'FirebaseFirestore', :git => 'https://github.com/invertase/firestore-ios-sdk-frameworks.git', :tag => '8.15.0'
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
end
結果
20-30秒程度でビルドできるようになった。
効果絶大だねw
┌─ Flutter Fix ────────────────────────────────────────────────────────────────────────────────────┐
│ The plugin cloud_firestore requires a higher Android SDK version. │
│ Fix this issue by adding the following to the file │
│ /Users/ryonishimura/Develop/flutter-codelabs/firebase-get-to-know-flutter/step_02/android/app/bu │
│ ild.gradle: │
│ android { │
│ defaultConfig { │
│ minSdkVersion 19 │
│ } │
│ } │
│ │
│ Note that your app won't be available to users running Android SDKs below 19. │
│ Alternatively, try to find a version of this plugin that supports these lower versions of the │
│ Android SDK. │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.gtk_flutter"
minSdkVersion 19
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
SDKを21以上にするとビルドできる
CloudFirestoreにメッセージを書き込む
CloudFirestore
- NoSQL
- データの種類
- コレクション
- ドキュメント
- フィールド
- サブコレクション
-
guestbook
と呼ばれるトップレベルのコレクション
実装
- フォームをインスタンス化し、メッセージに実際にコンテンツが含まれていることを検証する
- フォームの検証方法として、
GlobalKey
を使用
- フォームの検証方法として、
-
FirebaseAuth.instance.currentUser.uid
: ログインしているすべてのユーザーに提供される自動生成された一意のIDへの参照
結果
Firestore Database -> guestbook
基本的なセキュリティルールを設定する
- セキュリティルール
- DB内のドキュメントとコレクションへのアクセス制御
- 柔軟なルール構文を使用し、DB全体へのすべての書き込みから特定のドキュメントの操作まで、あらゆるものに一致するルールを作成可能
- 作成はFirebaseコンソールで可能
- [構築] ⇨ [Firestore Database] ⇨ [ルール]
コレクションを特定する
-
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.
}
}
セキュリティルールを追加
- 各
guestbook
ドキュメントのフィールドとして認証UIDを使用している- 認証UIDを取得し、ドキュメントに書き込もうとしているユーザーが一致する認証BUIDを持っていることを確認する
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;
}
}
}
完成版のmain.dart
最終的なセキュリティールール
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;
}
match /attendees/{userId} {
allow read: if true;
allow write: if request.auth.uid == userId
&& "attending" in request.resource.data;
}
}
}
GlobalKey
-
material.dart
ないの変数(Widgetの状態を管理する値)が変化したら画面を再描画させたい時に使う - WidgetTree全ての階層でアクセス可能
-
StatefulWidget
に対して利用する
https://qiita.com/kurun_pan/items/f91228cf5c793ec3f3cc
https://qiita.com/kurun_pan/items/0517fb62f1b47c90882c
FutureOr
Future<T> or なんかの値を返す
// void もしくは String型の引数を持つメソッド
FutureOr<void> Function(String message)
Firebase.initializeApp
- 名前&オプションで新しいFirebaseAppインスタンスを初期化し、作成されたアプリを返す
- FlutterFireのプラグインを使用する前に呼び出す
- Dartや手動設定時、
name
プロパティに値を渡さないとデフォルトのアプリインスタンスを初期化する
DefaultFirebaseOptions.currentPlatform
CLIで生成したファイルを参照し、プラットフォームごとの設定を呼び出す。
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
return web;
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
case TargetPlatform.iOS:
return ios;
case TargetPlatform.macOS:
return macos;
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
}
FirebaseFirestore.instance
デフォルトのFirebaseAppを返す
collection method
DocumentReferenceから指定されたパスにあるコレクションを参照する。そしてCollectionReferenceのインスタンスを取得する。
DocumentReference
DocumentReferenceは、FirebaseFirestoreDB内のドキュメントの場所を参照し、その場所への書き込み、読み込み、聞き取りに使用可能。
参照された場所のドキュメントは、存在する場合としない場合がある。DocumentReferenceは、サブコレクションへのCollectionReferenceをさくせいする際にも使用できる。
CollectionReference
CollectionReference は、ドキュメントの追加、ドキュメント参照の取得、およびドキュメントに対するクエリ(Query から継承したメソッドを使用)に使用可能。
注意:CloudFirestore クラスは、テストモックでの使用を除き、サブクラス化されることを意図していません。サブクラス化はプロダクションコードではサポートされておらず、新しい SDK リリースではサブクラス化したコードが破壊される可能性がある。
DocumentREference と CollecctionReferenceの違い
- DocumentReference : 一つのドキュメントを参照する
- CollectionReference : 複数ドキュメントのドキュメントを参照する
where method
指定したフィールドにフィルタをかけ新しいクエリを作成し、返す。
フィールドは、単一のフィールド名(ドキュメント内のトップレベルのフィールドを指す)からなる文字列もしくは、ドットで区切られた一連のフィールド名である可能性がある。あるいは、フィールドはFieldPathであることも可能である。
指定された条件を満たす文章のみが結果セットに含まれる。
snapshots method
ドキュメントの更新通知を受け取る。
最初のイベントがすぐに送信され、ドキュメントが変更されるたびにさらなるイベントが送信される。
/// Notifies of query results at this location.
Stream<QuerySnapshot<T>> snapshots({bool includeMetadataChanges = false});
userChanges method
ユーザーアップデートの変更について通知する。
authStateChangesとidTokenChangesのスーパーセット。
- クレデンシャル(認証情報)をリンクした
- クレデンシャルのリンクが解除
- ユーザープロファイルの変更
などユーザーの変更に関するイベントを提供する。
ユーザーの状態に対するリアルタイムの更新を手動で再読み込みせず、アプリケーション上でリアルタイムに変更を反映させるために使用する。
https://pub.dev/documentation/firebase_auth/latest/firebase_auth/FirebaseAuth/userChanges.html
authStateChages
orderBy
指定されたフィールドで追加的にソートされた新しいクエリを作成し、返す。
doc method
指定したパスのDocumentReferenceを返す。
パスが指定され得ていない場合、自動生成されたIDが使用され、クライアントが生成したタイムスタンプを先頭に持つたので結果のリストは時系列順でソートされる。
fetchSignInMethodsForEmail method
- メールアドレス認証のユーザーのサインインにしようできるメソッド一覧を返す
- メールファーストの認証フロー実装時に複数の認証機構をサポートするのに便利
- ユーザーが見つからない場合、空のリストが返る
https://pub.dev/documentation/firebase_auth/latest/firebase_auth/FirebaseAuth/fetchSignInMethodsForEmail.html
signInWithEmailAndPassword method
- 指定したメアドとパスワードでサインインを挑戦
- 成功時、アプリにユーザーをサインインする
- 各Streamのリスナーを更新する
- authStateChanges
- idTokenChanges
- userChanges
- 各Streamのリスナーを更新する
注意
メアドとパスワードでアカウントを使用する前にFirebaseコンソールのAuth sectionを有効にする必要がある。
もし設定していない場合、FirebaseAuthExceptionが以下のエラーコードを吐くことがある
- invalid-email : メールアドレスが有効でない
- user-disabled : 与えられたメールに対応するユーザが無効である
- user-not-found : 指定したメールに対応するユーザーが存在しない
- wrong-password : 与えられたメール対応するパスワードが無効。もしくは、メールに対応するアカウントにパスワードが設定されていない
createUserWithEmailAndPassword method
- 指定したメアドとパスワードで新しいユーザーアカウントを作成
エラーコード
FirebaseAuthExceptionで以下のエラーコードが表示される
- email-aiready-in-use : 指定したメールアドレスによるアカウントが既に存在している
- invalid-email : 指定したメアドが無効
- operation-not-allowed : メアドかパスワードがアカウントで有効じゃない場合に吐く
- weak-password : パスワードが十分に強くない場合に吐く
updateDisplayName method
ユーザー名を更新する
ApplicationStateのinit methodのフロー
- Firebaseのインスタンスを返す
- 「attendess」ドキュメントの「attendees」フィールドが trueの情報だけを{_attendees}に入れる
- ユーザーの登録情報を更新する
- ユーザーがログインしている
- {_loginState}をログイン状態に変更
- 「gesstbook」ドキュメントの「timestamp」フィールドの情報を降順にし取得する
- {_guestBookMessages}に取得した情報を入れていく
- 「attendess」ドキュメントにユーザーidが含まれているか
- 含まれている
- true : {_attending}をyesにする
- false : {_attending}をnoにする
- 含まれている
- 含まれていない
- {_attending}をunknownにする
- ユーザーがログアウトしている
- {_loginState}をログアウト状態に変更
- {_guestBookMessages}の中身を削除
- Streamの購読を廃止
- Google Photos Library APIはOAuth2.0を使用し、ユーザー認証が必須
- 各画面は個別のページとして実装
-
PhotosLibraryApiModel
はAPI呼び出しを抽象化 -
PhotosLibraryApiClient
は Library APIIへのRESTを定義し、*Request
で呼び出し