LoginSignup
1
1

FlutterでFirebaseのRealtime Databaseのちょっとしたまとめ

Last updated at Posted at 2024-03-25

はじめに

Firebase Realtime Databaseをなんとなく触っていてちゃんと理解していないので、少し理解度を高めるためにドキュメントを書きつつおさらいとお勉強。
まだ、orderByや絞り込みが十分ではないけど・・・

要するに、自分向けのメモ。

ちなみに本記事ではWindowsで開発を行い、Androidで実行している。
iOSはそのうち確認する。

※もし、間違ってること書いていたら教えていただけると助かります。
※今回のコード https://github.com/project-dev/flutter_firebase_realtime_database_test

Realtime Databaseの制限

公式ドキュメントの Realtime Database の制限事項 を参照するのが正解。

このドキュメントを書いている時点(2024/03/23)での制限事項でちょっと気になったところだけ。

多分使っていく間に制限事項にぶつかって理解できるものもありそう。

  • 同時接続は200,000が上限。Sparkプランは100が上限。
  • 1つのDBから同時に送信されるレスポンスの条件は100,000 件/秒
  • 子ノードのネストは32レベルまで
  • キーの長さは768バイト
  • 文字列の最大サイズは10MB
  • 1レスポンスの上限サイズは256MB
  • 1回の書き込みリクエストはREST APIからは256MB、SDKは16MB
  • 書き込みバイト数は64 MB/分

準備

基本的に公式の Flutter アプリに Firebase を追加する を参考にすればできる。

最初にFirebaseにログイン

> firebase login
Already logged in as <Googleアカウントのメールアドレス>

FlutterFire CLI をインストール

必要なパッケージが追加される。

> dart pub global activate flutterfire_cli
Package flutterfire_cli is currently active at version 0.2.7.
  cli_util 0.3.5 (0.4.1 available)
> collection 1.18.0 (was 1.17.2)
  deep_pick 0.10.0 (1.0.0 available)
> ffi 2.1.0 (was 2.0.2) (2.1.2 available)
  file 6.1.4 (7.0.0 available)
  http 0.13.6 (1.2.1 available)
  intl 0.18.1 (0.19.0 available)
> matcher 0.12.16+1 (was 0.12.16)
> meta 1.12.0 (was 1.9.1)
> path 1.9.0 (was 1.8.3)
> petitparser 6.0.2 (was 5.4.0)
> platform 3.1.4 (was 3.1.0)
  process 4.2.4 (5.0.2 available)
  pub_updater 0.2.4 (0.4.0 available)
> test_api 0.7.0 (was 0.6.1)
> win32 5.2.0 (was 5.0.5) (5.3.0 available)
> xml 6.5.0 (was 6.3.0)
Building package executables... (3.7s)
Built flutterfire_cli:flutterfire.
Installed executable flutterfire.
Activated flutterfire_cli 0.2.7.

firebaseの構成ワークフローを開始。

これやる前にfirebaseプロジェクトを作成しておくと既存のプロジェクトを利用できる。

ログの出方はfirebaseプロジェクトの数や設定により異なる。

> flutterfire configure   
i Found 4 Firebase projects.
✔ Select a Firebase project to configure your Flutter application with · <firebaseプロジェクトのID> (<firebaseプロジェクト名>)
✔ Which platforms should your configuration support (use arrow keys & space to select)? · android, ios
i Firebase android app <Flutterのプロジェクトのパッケージ名> is not registered on Firebase project <firebaseプロジェクトのID>.
i Registered a new Firebase android app on Firebase project <firebaseプロジェクトのID>.
i Firebase ios app <Flutterのプロジェクトのパッケージ名> is not registered on Firebase project <firebaseプロジェクトのID>.
i Registered a new Firebase ios app on Firebase project <firebaseプロジェクトのID>.

Firebase configuration file lib\firebase_options.dart generated successfully with the following Firebase apps:

Platform  Firebase App Id
android   1:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
ios       1:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Learn more about using this file and next steps from the documentation:
 > https://firebase.google.com/docs/flutter/setup

その後、必要なプラグインを追加する。

> flutter pub add firebase_core
Resolving dependencies... 
+ firebase_core 2.27.0 (2.27.1 available)
+ firebase_core_platform_interface 5.0.0
+ firebase_core_web 2.11.5 (2.12.0 available)
  flutter_lints 2.0.3 (3.0.1 available)
+ flutter_web_plugins 0.0.0 from sdk flutter
+ js 0.6.7 (0.7.1 available)
  lints 2.1.1 (3.0.0 available)
  matcher 0.12.16 (0.12.16+1 available)
  material_color_utilities 0.5.0 (0.11.1 available)
  meta 1.10.0 (1.12.0 available)
  path 1.8.3 (1.9.0 available)
+ plugin_platform_interface 2.1.8
  test_api 0.6.1 (0.7.0 available)
  web 0.3.0 (0.5.1 available)
Changed 6 dependencies!
11 packages have newer versions incompatible with dependency constraints.
Try `flutter pub outdated` for more information.

最後に、Firebase構成を最新にする。

> flutterfire configure
i Found 4 Firebase projects.
✔ Select a Firebase project to configure your Flutter application with · <firebaseプロジェクトのID> (<firebaseプロジェクト名>)
✔ Which platforms should your configuration support (use arrow keys & space to select)? · android, ios
i Firebase android app <Flutterのプロジェクトのパッケージ名> registered.
i Firebase ios app <Flutterのプロジェクトのパッケージ名> registered.

Firebase configuration file lib\firebase_options.dart generated successfully with the following Firebase apps:

Platform  Firebase App Id
android   1:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
ios       1:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Learn more about using this file and next steps from the documentation:
 > https://firebase.google.com/docs/flutter/setup

firebaseプラグインを追加したら flutterfire configure を実行するように公式ドキュメントから読み取れるけど、問題がなければやらなくてもよいのでは?と思っている。(が認識間違ってるかも?)

lib/main.dart でfirebaseを初期化する。

import 'package:flutter/material.dart';

// 以下2つを追加
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';

void main() async {
  
  // firebaseの初期化処理
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  runApp(const MyApp());
}

Realtime Databaseを使う

Realtime Databaseを使っていく。

データベース準備

Firebaseコンソールで構成からRealtime Databaseを選択してデータベースを作成する。

以下の画面が表示されるので、 データベースを作成 をクリックする。
image.png

データベースの設定ダイアログが表示される。
2024/03/23時点では以下の3つの選択肢がある。

  • 米国 (us-central1)
  • ベルギー (europe-west1)
  • シンガポール (asia-southeast1)

デフォルトは 米国 (us-central1)
今回はデフォルトのままで 次へ をクリック。

image.png

次にセキュリティルールの設定を行う。
ロックモードとテストモード存在する。
とりあえず使う場合はテストモードにするとよい。
ロックモードの場合、認証の実装も必要になる。
今回はテストモードにして 有効 をクリックする。
image.png

これでデータベースが生成される。
以下のスクリーンショットのようにデータを作成する。
image.png

flutter側の準備

Realtime Databaseプラグインを追加する。

> flutter pub add firebase_database
Resolving dependencies... 
+ _flutterfire_internals 1.3.25 (1.3.26 available)
  firebase_core 2.27.0 (2.27.1 available)
  firebase_core_web 2.11.5 (2.12.0 available)
+ firebase_database 10.4.9 (10.4.10 available)
+ firebase_database_platform_interface 0.2.5+25 (0.2.5+26 available)
+ firebase_database_web 0.2.3+25 (0.2.3+26 available)
  flutter_lints 2.0.3 (3.0.1 available)
  js 0.6.7 (0.7.1 available)
  lints 2.1.1 (3.0.0 available)
  matcher 0.12.16 (0.12.16+1 available)
  material_color_utilities 0.5.0 (0.11.1 available)
  meta 1.10.0 (1.12.0 available)
  path 1.8.3 (1.9.0 available)
  test_api 0.6.1 (0.7.0 available)
  web 0.3.0 (0.5.1 available)
Changed 4 dependencies!
15 packages have newer versions incompatible with dependency constraints.
Try `flutter pub outdated` for more information.

flutter configre を実行

> flutterfire configure
i Found 4 Firebase projects.
✔ Select a Firebase project to configure your Flutter application with · <firebaseプロジェクトのID> (<firebaseプロジェクト名>)
✔ Which platforms should your configuration support (use arrow keys & space to select)? · android, ios
i Firebase android app <Flutterのプロジェクトのパッケージ名> registered.
i Firebase ios app <Flutterのプロジェクトのパッケージ名> registered.

Firebase configuration file lib\firebase_options.dart generated successfully with the following Firebase apps:

Platform  Firebase App Id
android   1:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
ios       1:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Learn more about using this file and next steps from the documentation:
 > https://firebase.google.com/docs/flutter/setup

Realtime Databaseのインスタンスの取得

いくつか方法があり、詳しくは公式ドキュメントの Firebase Realtime Database パッケージを初期化する を参照するのが良い。

今回は以下のようにインスタンスを取得する。

FirebaseDatabase database = FirebaseDatabase.instance;

読み込み

データの読み取りと書き込み を参考にするとよい。

読み込み方法を3つ。
目的や用途に応じて利用するのがよさそう。
通信量にかかわってくると思われるので、Realtime Databaseの制限や課金を注意すべし。

get()メソッドで値を取得する

import 'dart:developer' as developer;
・・・略・・・
                final ref = FirebaseDatabase.instance.ref('users/1');
                final snapshot = await ref.get();
                if(snapshot.value != null){
                  developer.log("value : ${snapshot.value}");
                }else{
                  developer.log("value : null");
                }

結果

[log] value : {name: name1, age: 18}

リスナーを使用する

  @override
  void initState() {
    super.initState();

    final ref = FirebaseDatabase.instance.ref('users');
    ref.onValue.listen((event) {
      final snapshot = event.snapshot;
      if (snapshot.value != null) {
        developer.log("value : ${snapshot.value}");
      } else {
        developer.log("value : null");
      }
    });
  }

起動時や変更があった場合に値が取得される。
以下はageを18から19に変更した場合の結果。

[log] value : [null, {name: name1, age: 18}, {name: name2, age: 32}]
[log] value : [null, {name: name1, age: 19}, {name: name2, age: 32}]

リスナーを使うことで、Webの更新をスマホでリアルタイムに反映する、といったことができそうだが、イベントの発生元がわからないことと、更新したクライアントもリスナーが呼び出されるので工夫も必要かもしれない。
(データ更新時に更新ユーザーのIDを含めるとか)
※もしわかる方法があれば教えてください。

once()メソッドを使った場合

ローカルキャッシュからの読み込みを行うらしい。
変更があまり発生しないものや、変更を即時に反映しないデータなどを読み込む場合に利用することが想定されているらしい。

                final ref = FirebaseDatabase.instance.ref('users/1');
                final event = await ref.once(DatabaseEventType.value);
                if(event.snapshot.value != null){
                  developer.log("value : ${event.snapshot.value}");
                }else{
                  developer.log("value : null");
                }

書き込みと更新

データの読み取りと書き込み を参考にするとよい。

書き込みに関する方法を2つ。

set()メソッド

指定したデータを挿入できる。

                  final ref = FirebaseDatabase.instance.ref('users/3');
                  await ref.set({
                    'name':'name3',
                    'age':'42'
                  });

結果、データが以下のように追加される。
image.png

複数回呼び出しても、同じものは追加されない。
リスナーを設定している場合、通知が来るので、リスナーの設定等考慮が必要なケースがありそう。

update()メソッド

set()メソッドの場合、該当するノードを子ノード含め更新される。
特定のノードを更新したい場合はupdate()メソッドを利用する。

                final ref = FirebaseDatabase.instance.ref('users/3');
                await ref.update({
                  'age':'44'
                });

push()メソッド

一意のキーを生成し、set()メソッドなどで値を設定することでデータを登録できる。
push()メソッドを呼び出しただけではデータベースにデータは追加されないので注意。

                final ref = FirebaseDatabase.instance.ref('users');
                var newRef = ref.push();
                await newRef.set({
                  'name':'name4',
                  'age':'55'
                });

結果
image.png

削除

データの読み取りと書き込み を参考にするとよい。

削除はset()メソッドやupdate()メソッドでnullを指定することで可能。

                final ref = FirebaseDatabase.instance.ref('users/3');
                await ref.set(null);

トランザクション

RDBのようにトランザクションを利用することもできる。

                final ref = FirebaseDatabase.instance.ref('users/3');
                var result = await ref.runTransaction((value){
                  if(value == null){
                    return Transaction.abort();
                  }
                  Map<String, dynamic> post = Map<String, dynamic>.from(value as Map);
                  post['name'] = 'hogehoge';
                  post['age'] = 101;
                  return Transaction.success(post);
                });

Transaction.success(null)とすると削除も行える。

オフラインでの挙動

公式ドキュメントの オフライン機能の有効化 を参照するとよい。

Firebase Realtime Databaseではオフライン時のデータの変更も保持する。
アプリ再起動やOS再起動を行っても保持させるためには以下の処理を呼び出し、永続化を行う必要がある。

FirebaseDatabase.instance.setPersistenceEnabled(true);

この呼び出しはmain()メソッド内で呼び出せばよい。

void main() async {

  WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized();

  // firebaseの初期化処理
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  FirebaseDatabase.instance.setPersistenceEnabled(true);

  runApp(const MyApp());
}

注意点として、トランザクションで処理された内容は保持されないため、オフラインの挙動を想定したアプリケーションの開発は注意が必要となる。

以下はオフラインからの復旧時の挙動をまとめたもの。

状態 永続化有効 永続化無効 オフライン時のトランザクションの変更
オフライン→オンライン
オフライン→アプリ再起動→オンライン
オフライン→端末再起動→オンライン

※"オフライン時のトランザクションの変更" は永続化有効時の状態

Realtime Databaseをさらに使いこなす

並び替えや絞り込み、インデックス等についてのメモ

データを並び替えて取得

公式ドキュメントの データのリストを操作する を参照する。

並び替えのメソッドはいくつか存在する。
最初に注意点として挙げておくと、昇順しか存在しない。
降順でデータを取得したい場合は工夫が必要になる。

以下のデータを取得する
image.png

データは
1,2,3,-NtkDeV87ey67lG8dFPq,10の順番で挿入。

公式ドキュメントだけだとわかりづらく、firebase_databaseのChangelogの9.0.0にsnapshot.valueは並び替えが行われないから、ソートした結果はsnapshot.childrenを使え的なことが記載されている。

試したところ、以下の結果となった。
once()メソッドで取得した結果に対し、childrenを使うことで並び順が反映された結果が取得できる。
※この辺、公式ドキュメントにちゃんと書いてほしい・・・見つけられてないだけかもしれないが

取得メソッド 参照 並び順の反映
get() value
get() children
once() value
once() children

確認コード

                final ref = FirebaseDatabase.instance.ref('users');
                final query = ref.orderByChild('age');

                final snapshot = await query.get();
                developer.log("- get() -> value --------------------------------");
                developer.log("value_1 : ${snapshot.value}");

                developer.log("- get() -> children --------------------------------");
                for (var element in snapshot.children) {
                  developer.log("value_2 : ${element.value}");
                }

                developer.log("- once() -> children --------------------------------");
                final onceEvent1 = await query.once();
                for (var element in onceEvent1.snapshot.children) {
                  developer.log("value_3 : ${element.value}");
                }

                developer.log("- once() -> value --------------------------------");
                var items3 = onceEvent1.snapshot.value;
                developer.log("value_4 : $items3");

結果

[log] - get() -> value --------------------------------
[log] value_1 : {1: {name: name1, age: 18}, 2: {name: name2, age: 32}, 3: {name: hogehoge, age: 101}, -NtkDeV87ey67lG8dFPq: {name: name4, age: 55}, 10: {name: name10, age: 2}}
[log] - get() -> children --------------------------------
[log] value_2 : {name: name1, age: 18}
[log] value_2 : {name: name2, age: 32}
[log] value_2 : {name: hogehoge, age: 101}
[log] value_2 : {name: name10, age: 2}
[log] value_2 : {name: name4, age: 55}
[log] - once() -> children --------------------------------
[log] value_3 : {name: name10, age: 2}
[log] value_3 : {name: name1, age: 18}
[log] value_3 : {name: name2, age: 32}
[log] value_3 : {name: name4, age: 55}
[log] value_3 : {name: hogehoge, age: 101}
[log] - once() -> value --------------------------------
[log] value_4 : {1: {name: name1, age: 18}, 2: {name: name2, age: 32}, 3: {name: hogehoge, age: 101}, -NtkDeV87ey67lG8dFPq: {name: name4, age: 55}, 10: {name: name10, age: 2}}

データを絞り込んで取得

公式ドキュメントの データのリストを操作する を参照する。

絞り込みはorderByで並び替えを行いつつ絞り込みをかける。

まずはorderByを行わずにstartAt()メソッドを呼び出した場合。

                final ref = FirebaseDatabase.instance.ref('users');
                final query = ref.startAt(20, key:'age');

                final snapshot = await query.get();
                developer.log("- get() -> value --------------------------------");
                developer.log("value_1 : ${snapshot.value}");

                developer.log("- get() -> children --------------------------------");
                for (var element in snapshot.children) {
                  developer.log("value_2 : ${element.value}");
                }

                developer.log("- once() -> children --------------------------------");
                final onceEvent1 = await query.once();
                for (var element in onceEvent1.snapshot.children) {
                  developer.log("value_3 : ${element.value}");
                }

                developer.log("- once() -> value --------------------------------");
                var items3 = onceEvent1.snapshot.value;
                developer.log("value_4 : $items3");

結果

[log] - get() -> value --------------------------------
[log] value_1 : null
[log] - get() -> children --------------------------------
[log] - once() -> children --------------------------------
[log] - once() -> value --------------------------------
[log] value_4 : null

次に、orderByとstartAt()メソッドを呼び出した場合。

                final ref = FirebaseDatabase.instance.ref('users');
                final query = ref.orderByChild('age').startAt(20, key:'age');

                final snapshot = await query.get();
                developer.log("- get() -> value --------------------------------");
                developer.log("value_1 : ${snapshot.value}");

                developer.log("- get() -> children --------------------------------");
                for (var element in snapshot.children) {
                  developer.log("value_2 : ${element.value}");
                }

                developer.log("- once() -> children --------------------------------");
                final onceEvent1 = await query.once();
                for (var element in onceEvent1.snapshot.children) {
                  developer.log("value_3 : ${element.value}");
                }

                developer.log("- once() -> value --------------------------------");
                var items3 = onceEvent1.snapshot.value;
                developer.log("value_4 : $items3");

結果

[log] - get() -> value --------------------------------
[log] value_1 : {2: {name: name2, age: 32}, 3: {name: hogehoge, age: 101}, -NtkDeV87ey67lG8dFPq: {name: name4, age: 55}}
[log] - get() -> children --------------------------------
[log] value_2 : {name: name2, age: 32}
[log] value_2 : {name: hogehoge, age: 101}
[log] value_2 : {name: name4, age: 55}
[log] - once() -> children --------------------------------
[log] value_3 : {name: name2, age: 32}
[log] value_3 : {name: name4, age: 55}
[log] value_3 : {name: hogehoge, age: 101}
[log] - once() -> value --------------------------------
[log] value_4 : {2: {name: name2, age: 32}, 3: {name: hogehoge, age: 101}, -NtkDeV87ey67lG8dFPq: {name: name4, age: 55}}

startAt()メソッド以外は試していないのでわからないが、もし、絞り込みが聞かない場合はorderByと組み合わせるのがよさそう。

インデックスの設定

公式ドキュメントの データのインデックス作成 を参照。

ルールに記載することで設定できる。

{
  "rules": {
    ".read": true,
    ".write": true,
    "users": {
      ".indexOn": ["age"]
    }
  }
}

トラブル対応

firebase configure が失敗する

以下の問題が発生した

> flutterfire configure
i Found 0 Firebase projects.
FirebaseCommandException: An error occured on the Firebase CLI when attempting to run a command.
COMMAND: firebase projects:list --json
ERROR: Failed to list Firebase projects. See firebase-debug.log for more info.

この場合、 firebase logout して firebase login することで回避できた。

GitHubでPublicで公開するとき

Google API Key等の情報が入ったファイルをpushすると、GitHubから怒られる。

image.png

これを回避するために、以下を.gitignoreに入れるとよい

ファイル 設定
android.gitignore !*.gitignore
google-services.json
ios.gitignore !*.gitignore
GoogleService-Info.plist
.gitignore !*..gitignore
firebase_options.dart

!*.gitignore を設定しないと、.gitignore自体が除外されてGitHubに反映されないため。

参考

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1