Open18

FlutterFireを勉強するスレ

Ryo24Ryo24

FirebaseCLIをインストール

brewでCLIをインストール
brew install firebase-cli

Firebase CLIが使えるかテスト

Googleアカウントを選択し、Firebaseにログイン
firebase login

コマンドを入力すると

  1. Firebaseのエラーレポートの報告の有無の確認
  2. ブラウザが起動され、Googleアカウントの選択
  3. 成功したら、「Success! Logged in as {Googleアカウント名}」と表示される

アカウントに正常にログインできるかテストする

firebase projects:list
Ryo24Ryo24
サンプルプロジェクトをクローン
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のメールベース認証の処理が含まれる
Ryo24Ryo24

Firebaseのプロジェクトを作成する

https://console.firebase.google.com/
ログインし、プロジェクトを作成する

Firebase認証のメールログインを有効化

  1. 左のタブから構築 -> Authentication
  2. 上に表示されている「始める」をクリック
  3. Sign-in method -> 「メール/パスワード」⇨ 有効にする ⇨ 保存

CloudFirestoreを有効化

Webアプリは、CloudFirestoreを使用しチャットメッセージを保存し、新しいチャットメッセージを受信する。

  1. 構築 ⇨ Firestore Database -> 「データベースの作成」をクリック
  2. 「テストモードで開始する」を選択
  3. 「データベスの作成」項目では、データベースの場所を選択する。この場所は後で変更できない。

    https://firebase.google.com/docs/firestore/locations?hl=ja
    基本的には、「asia-northeast1」(東京)を選択すればOK
Ryo24Ryo24

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に依存する。

FlutterFireCLIをインストール
dart pub global activate flutterfire_cli

// Flutter SDKを保存しているディレクトリに移動し、パスを通す
export PATH="$PATH":"$HOME/.pub-cache/bin"

アプリの構成

iOS,Android,Webの構成を自動生成してくれる

アプリケーションのルートで実行
flutterfire configure

実行すると

  1. Firebaseプロジェクトの選択
  2. 構成するプラットフォームの選択
  3. 選択したプラットフォーム構成を抽出。デフォルトは、現在のプロジェクト構成に基づき自動的に照合させる。
  4. プロジェクトでfirebase_options.dartファイルを生成する

コマンド実行後の状態

git status
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を構成する

macOSの設定に関する詳細

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>
Ryo24Ryo24

ユーザーサインインの追加

main.dartの編集

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を編集してがエラーを吐く。この編集をするためには、

  1. Podfileを編集する
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,
}
  1. iosのファオルダに移動し、pod install
  2. flutter clean
  3. flutter pub get
  4. Xcodeを再起動&実行

参考サイト

Ryo24Ryo24

iOSのビルド時間を高速化

Firebaseを導入した7-8分ほどビルドに要した。

https://zenn.dev/nagaho/articles/012e9ac3b0dfd1

上記の記事曰く、

  • Firestore iOS SDKがC++の50万行でできている
  • ビルド時、Flutterのプログラム&Firebaseのプログラムをビルドしている
  • firestore-ios-sdk-frameworksを導入する
  • 「firestore-ios-sdk-frameworks」は、プリコンパイルされたやつ

https://github.com/invertase/firestore-ios-sdk-frameworks

Podfileを編集

ios/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

Ryo24Ryo24
デフォルトで実行時のエラー
┌─ 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.                                                                                     │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘
SDKを19以上にしてもエラーを吐く
    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
    }

https://qiita.com/hayashi-ay/items/a7a06b6f1fb0a8a25dcf

SDKを21以上にするとビルドできる

Ryo24Ryo24

CloudFirestoreにメッセージを書き込む

CloudFirestore

  • NoSQL
  • データの種類
    • コレクション
    • ドキュメント
    • フィールド
    • サブコレクション
  • guestbookと呼ばれるトップレベルのコレクション

実装

  • フォームをインスタンス化し、メッセージに実際にコンテンツが含まれていることを検証する
    • フォームの検証方法として、GlobalKeyを使用
  • FirebaseAuth.instance.currentUser.uid : ログインしているすべてのユーザーに提供される自動生成された一意のIDへの参照

結果

Firestore Database -> guestbook

Ryo24Ryo24

基本的なセキュリティルールを設定する

  • セキュリティルール
    • DB内のドキュメントとコレクションへのアクセス制御
    • 柔軟なルール構文を使用し、DB全体へのすべての書き込みから特定のドキュメントの操作まで、あらゆるものに一致するルールを作成可能
    • 作成はFirebaseコンソールで可能
      • [構築] ⇨ [Firestore Database] ⇨ [ルール]

https://firebase.google.com/docs/rules
https://www.youtube.com/watch?v=QEuu9X9L-MU&list=PLl-K7zZEsYLn8h1NyU_OV6dX8mBhH2s_L

コレクションを特定する

  • 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;
    }
  }
}
Ryo24Ryo24

完成版のmain.dart

https://github.com/flutter/codelabs/blob/master/firebase-get-to-know-flutter/step_09/lib/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;
    }
  }
}
Ryo24Ryo24

GlobalKey

FutureOr

Future<T> or なんかの値を返す

// void もしくは String型の引数を持つメソッド
FutureOr<void> Function(String message)

https://api.flutter.dev/flutter/dart-async/FutureOr-class.html

Firebase.initializeApp

  • 名前&オプションで新しいFirebaseAppインスタンスを初期化し、作成されたアプリを返す
  • FlutterFireのプラグインを使用する前に呼び出す
  • Dartや手動設定時、nameプロパティに値を渡さないとデフォルトのアプリインスタンスを初期化する

https://pub.dev/documentation/firebase_core/latest/firebase_core/Firebase/initializeApp.html

DefaultFirebaseOptions.currentPlatform

CLIで生成したファイルを参照し、プラットフォームごとの設定を呼び出す。

lib/firebase_options.dart
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を返す
https://pub.dev/documentation/cloud_firestore/latest/cloud_firestore/FirebaseFirestore/instance.html

collection method

DocumentReferenceから指定されたパスにあるコレクションを参照する。そしてCollectionReferenceのインスタンスを取得する。
https://pub.dev/documentation/cloud_firestore/latest/cloud_firestore/DocumentReference/collection.html

DocumentReference

DocumentReferenceは、FirebaseFirestoreDB内のドキュメントの場所を参照し、その場所への書き込み、読み込み、聞き取りに使用可能。
参照された場所のドキュメントは、存在する場合としない場合がある。DocumentReferenceは、サブコレクションへのCollectionReferenceをさくせいする際にも使用できる。
https://pub.dev/documentation/cloud_firestore/latest/cloud_firestore/DocumentReference-class.html

CollectionReference

CollectionReference は、ドキュメントの追加、ドキュメント参照の取得、およびドキュメントに対するクエリ(Query から継承したメソッドを使用)に使用可能。
注意:CloudFirestore クラスは、テストモックでの使用を除き、サブクラス化されることを意図していません。サブクラス化はプロダクションコードではサポートされておらず、新しい SDK リリースではサブクラス化したコードが破壊される可能性がある。

https://firebase.google.com/docs/reference/android/com/google/firebase/firestore/CollectionReference

DocumentREference と CollecctionReferenceの違い

  • DocumentReference : 一つのドキュメントを参照する
  • CollectionReference : 複数ドキュメントのドキュメントを参照する

https://qiita.com/maiyama18/items/86a4573fdce800221b72#参照collectionreferencequerydocumentreference

where method

指定したフィールドにフィルタをかけ新しいクエリを作成し、返す。
フィールドは、単一のフィールド名(ドキュメント内のトップレベルのフィールドを指す)からなる文字列もしくは、ドットで区切られた一連のフィールド名である可能性がある。あるいは、フィールドはFieldPathであることも可能である。
指定された条件を満たす文章のみが結果セットに含まれる。
https://pub.dev/documentation/cloud_firestore/latest/cloud_firestore/Query/where.html

snapshots method

ドキュメントの更新通知を受け取る。
最初のイベントがすぐに送信され、ドキュメントが変更されるたびにさらなるイベントが送信される。

定義
/// Notifies of query results at this location.
  Stream<QuerySnapshot<T>> snapshots({bool includeMetadataChanges = false});

https://pub.dev/documentation/cloud_firestore/latest/cloud_firestore/DocumentReference/snapshots.html

Ryo24Ryo24

userChanges method

ユーザーアップデートの変更について通知する。
authStateChangesとidTokenChangesのスーパーセット。

  • クレデンシャル(認証情報)をリンクした
  • クレデンシャルのリンクが解除
  • ユーザープロファイルの変更
    などユーザーの変更に関するイベントを提供する。
    ユーザーの状態に対するリアルタイムの更新を手動で再読み込みせず、アプリケーション上でリアルタイムに変更を反映させるために使用する。
    https://pub.dev/documentation/firebase_auth/latest/firebase_auth/FirebaseAuth/userChanges.html

authStateChages

orderBy

指定されたフィールドで追加的にソートされた新しいクエリを作成し、返す。
https://pub.dev/documentation/cloud_firestore/latest/cloud_firestore/Query/orderBy.html

doc method

指定したパスのDocumentReferenceを返す。
パスが指定され得ていない場合、自動生成されたIDが使用され、クライアントが生成したタイムスタンプを先頭に持つたので結果のリストは時系列順でソートされる。
https://pub.dev/documentation/cloud_firestore/latest/cloud_firestore/CollectionReference/doc.html

Ryo24Ryo24

fetchSignInMethodsForEmail method

signInWithEmailAndPassword method

  • 指定したメアドとパスワードでサインインを挑戦
  • 成功時、アプリにユーザーをサインインする
    • 各Streamのリスナーを更新する
      • authStateChanges
      • idTokenChanges
      • userChanges

注意

メアドとパスワードでアカウントを使用する前にFirebaseコンソールのAuth sectionを有効にする必要がある。
もし設定していない場合、FirebaseAuthExceptionが以下のエラーコードを吐くことがある

  • invalid-email : メールアドレスが有効でない
  • user-disabled : 与えられたメールに対応するユーザが無効である
  • user-not-found : 指定したメールに対応するユーザーが存在しない
  • wrong-password : 与えられたメール対応するパスワードが無効。もしくは、メールに対応するアカウントにパスワードが設定されていない

https://pub.dev/documentation/firebase_auth/latest/firebase_auth/FirebaseAuth/signInWithEmailAndPassword.html

createUserWithEmailAndPassword method

  • 指定したメアドとパスワードで新しいユーザーアカウントを作成

エラーコード

FirebaseAuthExceptionで以下のエラーコードが表示される

  • email-aiready-in-use : 指定したメールアドレスによるアカウントが既に存在している
  • invalid-email : 指定したメアドが無効
  • operation-not-allowed : メアドかパスワードがアカウントで有効じゃない場合に吐く
  • weak-password : パスワードが十分に強くない場合に吐く

https://pub.dev/documentation/firebase_auth/latest/firebase_auth/FirebaseAuth/createUserWithEmailAndPassword.html

updateDisplayName method

ユーザー名を更新する
https://pub.dev/documentation/firebase_auth/latest/firebase_auth/User/updateDisplayName.html

Ryo24Ryo24

ApplicationStateのinit methodのフロー

  1. Firebaseのインスタンスを返す
  2. 「attendess」ドキュメントの「attendees」フィールドが trueの情報だけを{_attendees}に入れる
  3. ユーザーの登録情報を更新する
  • ユーザーがログインしている
    • {_loginState}をログイン状態に変更
    • 「gesstbook」ドキュメントの「timestamp」フィールドの情報を降順にし取得する
    • {_guestBookMessages}に取得した情報を入れていく
    • 「attendess」ドキュメントにユーザーidが含まれているか
      • 含まれている
        • true : {_attending}をyesにする
        • false : {_attending}をnoにする
    • 含まれていない
      • {_attending}をunknownにする
  • ユーザーがログアウトしている
    • {_loginState}をログアウト状態に変更
    • {_guestBookMessages}の中身を削除
    • Streamの購読を廃止
Ryo24Ryo24
  • 各画面は個別のページとして実装
  • PhotosLibraryApiModelはAPI呼び出しを抽象化
  • PhotosLibraryApiClient は Library APIIへのRESTを定義し、*Requestで呼び出し