snovaのブログ

主にプログラミングやデジタルコンテンツについて書きます。最近はPython, Flutter, VRに興味があります。

Flutterから利用できるFirebaseサービスをカウンターアプリで実践(Realtime Database編)

はじめに

Flutterから利用できるFirebaseの機能をカウンターアプリに実装し、内容をまとめたシリーズの第7回目です。

今回は、NoSQLによりデータの保管とすべてのクライアントにわたるリアルタイム同期を行うことができるFirebase Realtime Databaseについてです。

データはjsonとして保存され、データ量に応じて課金される仕組みなので、Firestoreと比較して頻度が高く小さなデータを保存することに向いています。 公式サイトにFirestoreとの比較がありますので、そのプロジェクトでどのデータベースを選択するかの参考になります。

目次

シリーズの内容

回数 内容 リンク
第0回 準備編 ブログ
第1回 Analytics ブログ
第2回 Firebase Crashlytics ブログ
第3回 Firebase Performance Monitoring ブログ
第4回 Firebase Remote Config ブログ
第5回 Firebase Authentication ブログ
第6回 Cloud Firestore ブログ
第7回 Firebase Realtime Database イマココ
第8回 Cloud Storage for Firebase ブログ
第9回 Firebase Cloud Messaging ブログ
第10回 Firebase In-App Messaging ブログ
第11回 Firebase ML ブログ
第12回 Cloud Functions for Firebase ブログ
第13回 Firebase Hosting ブログ
第14回 Firebaseのその他のサービス ブログ

開発環境

項目 内容
PC Macbook Air(M1)
Flutter 3.0.1
Firebase 11.0.1
FlutterFire 0.2.2+2
デバッグバイス Android 12(APIレベル31), Chrome

準備

準備編が完了出来ているものとします。

導入方法

Firebase Consoleから、Realtime Databaseを選択し、データベースの作成を行います。 リージョンはasia-southeast1(シンガポール)、セキュリティ設定は「ロックモード」を選択します。

前回の記事にも記載しましたが、Realtime DatabaseでもFirebaseセキュリティルールを設定する必要があります。

公式によると、Cloud Firestoreルールと Cloud Storageルールでは、Common Expression Language(CEL)をベースとした言語を使用していますが、Realtime Databaseではjsonの構文を使用しています。

今回はコンテンツ所有者のみがアクセスできるように、以下の通り設定しました。

{
  "rules": {
    "users": {
      "$uid": {
        ".read": "auth != null && auth.uid == $uid",
        ".write": "auth != null && auth.uid == $uid"
      }
    }
  }
}

Realtime Databaseの構造はjsonです。

Firebase Consoleでの設定が終わったら、プロジェクトにfirebase_databaseを導入するため、pubspec.yamlに追記しインポートします。

dependencies:
  firebase_database: ^9.0.14

ユーザーIDの取得とデータベースの定義を行います。 読み書きにはRiverpodを使用しています。

import 'package:firebase_database/firebase_database.dart';
import 'package:firebase_auth/firebase_auth.dart';

/// UserIDの取得
final userID = FirebaseAuth.instance.currentUser?.uid ?? '';

/// Realtime Databaseのデータベース定義
DatabaseReference dbRef = FirebaseDatabase.instance.ref('users');

Realtime Databaseへのデータの書込には、setを使う方法とupdateを使う方法があります。 今回はupdateで実装しました。

void write(WidgetRef ref) async {
  try {
    await dbRef.update({
      '$userID/count': ref.read(counterProvider),
    });
  } catch (e) {
    print('Error : $e');
  }
}

Realtime Databaseのデータ読込には、DatabaseReferenceをリッスンしDatabaseEventを呼ぶ方法とget()を使用する方法があります。 前者はデータが変更されるたびにトリガーされますが、後者はデータを1回だけ読み取ります。 今回は、get()で実装しました。

void read(WidgetRef ref) async {
  try {
    final snapshot = await dbRef.child(userID).get();
    if (snapshot.exists) {
      ref.read(counterProvider.notifier).state =
          snapshot.child('count').value as int;
    }
  } catch (e) {
    print('Error : $e');
  }
}

データの削除remove()を使用します。

/// Realtime Databaseのデータ削除
void remove() async {
  try {
    await dbRef.child(userID).remove();
  } catch (e) {
    print('Error : $e');
  }
}

画面

実行後、Firebase Consoleでデータベースが変更されていることを確認します。

コード全文

前回からの変更点です。

  • Realtime Databaseを実装
  • Realtime Databaseのページを追加
  • その他、コードの変更

main.dart

/// Flutter関係のインポート
import 'package:counter_firebase/firestore_page.dart';
import 'package:counter_firebase/realtime_database_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'dart:async';

/// Firebase関係のインポート
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:firebase_auth/firebase_auth.dart';

/// 他ページのインポート
import 'package:counter_firebase/normal_counter_page.dart';
import 'package:counter_firebase/crash_page.dart';
import 'package:counter_firebase/auth_page.dart';
import 'package:counter_firebase/remote_config_page.dart';

/// メイン
void main() async {
  /// クラッシュハンドラ
  runZonedGuarded<Future<void>>(() async {
    /// Firebaseの初期化
    WidgetsFlutterBinding.ensureInitialized();

    await Firebase.initializeApp(
      name: 'counterFirebase',
      options: DefaultFirebaseOptions.currentPlatform,
    );

    /// クラッシュハンドラ(Flutterフレームワーク内でスローされたすべてのエラー)
    FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;

    /// runApp w/ Riverpod
    runApp(const ProviderScope(child: MyApp()));
  },

      /// クラッシュハンドラ(Flutterフレームワーク内でキャッチされないエラー)
      (error, stack) =>
          FirebaseCrashlytics.instance.recordError(error, stack, fatal: true));
}

/// Providerの初期化
/// カウンター用のプロバイダー
final counterProvider = StateNotifierProvider.autoDispose<Counter, int>((ref) {
  return Counter();
});

class Counter extends StateNotifier<int> {
  Counter() : super(0);

  /// カウントアップ
  void increment() => state++;
}

/// MaterialAppの設定
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Counter Firebase',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
      debugShowCheckedModeBanner: false,
    );
  }
}

/// ホーム画面
class MyHomePage extends ConsumerWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    /// ログイン状態の確認
    FirebaseAuth.instance.authStateChanges().listen(
      (User? user) {
        if (user == null) {
          ref.watch(userEmailProvider.state).state = 'ログインしていません';
        } else {
          ref.watch(userEmailProvider.state).state = user.email!;
        }
      },
    );

    return Scaffold(
      appBar: AppBar(
        title: const Text('My Homepage'),
      ),
      body: ListView(
        padding: const EdgeInsets.all(10),
        children: <Widget>[
          /// ユーザ情報の表示
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const Icon(Icons.person),
              Text(ref.watch(userEmailProvider)),
            ],
          ),

          /// 各ページへの遷移
          _PagePushButton(
              context, 'ノーマルカウンター', const NormalCounterPage(), Colors.blue),
          _PagePushButton(context, 'クラッシュページ', const CrashPage(), Colors.blue),
          _PagePushButton(context, 'Remote Configカウンター',
              const RemoteConfigPage(), Colors.blue),
          _PagePushButton(context, '認証ページ', const AuthPage(), Colors.red),

          /// 各ページへの遷移(認証後利用可能)
          /// 認証されていなかったらボタンを押せない状態にする
          FirebaseAuth.instance.currentUser?.uid != null
              ? _PagePushButton(context, 'Firestoreカウンター',
                  const FirestorePage(), Colors.green)
              : const Text('Firestoreカウンターを開くためには認証してください。'),
          FirebaseAuth.instance.currentUser?.uid != null
              ? _PagePushButton(context, 'Realtime Databaseカウンター',
                  const RealtimeDatabasePage(), Colors.green)
              : const Text('Realtime Databaseカウンターを開くためには認証してください。'),
        ],
      ),
    );
  }
}

/// ページ遷移ボタン
class _PagePushButton extends Container {
  _PagePushButton(
      BuildContext context, String buttonTitle, pagename, Color bgColor)
      : super(
          padding: const EdgeInsets.all(10),
          child: ElevatedButton(
            onPressed: () {
              AnalyticsService().logPage(buttonTitle);
              Navigator.push(
                  context, MaterialPageRoute(builder: (context) => pagename));
            },
            style: ButtonStyle(
                backgroundColor: MaterialStateProperty.all(bgColor)),
            child: Text(buttonTitle),
          ),
        );
}

/// Analyticsの実装
class AnalyticsService {
  /// ページ遷移のログ
  Future<void> logPage(String screenName) async {
    await FirebaseAnalytics.instance.logEvent(
      name: 'screen_view',
      parameters: {
        'firebase_screen': screenName,
      },
    );
  }
}

realtime_database_page.dart

/// Flutter
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

/// Firebase
import 'package:firebase_database/firebase_database.dart';
import 'package:firebase_auth/firebase_auth.dart';

/// Other Page
import 'package:counter_firebase/main.dart';

class RealtimeDatabasePage extends ConsumerStatefulWidget {
  const RealtimeDatabasePage({Key? key}) : super(key: key);

  @override
  RealtimeDatabasePageState createState() => RealtimeDatabasePageState();
}

class RealtimeDatabasePageState extends ConsumerState<RealtimeDatabasePage> {
  @override
  void initState() {
    super.initState();

    /// Realtime Databaseの数値を読取
    RealtimeDatabaseService().read(ref);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Realtime Databaseカウンター'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '${ref.watch(counterProvider)}',
              style: Theme.of(context).textTheme.headline4,
            ),

            /// カウントをリセットし、Realtime Databaseからも削除する
            TextButton(
                onPressed: () {
                  ref.watch(counterProvider.notifier).state = 0;
                  RealtimeDatabaseService().remove();
                },
                child: const Text('Reset')),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          ref.read(counterProvider.notifier).increment();
          RealtimeDatabaseService().write(ref);
        },
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

/// Realtime Databaseの設定
class RealtimeDatabaseService {
  /// UserIDの取得
  final userID = FirebaseAuth.instance.currentUser?.uid ?? '';

  /// Realtime Databaseのデータベース定義
  DatabaseReference dbRef = FirebaseDatabase.instance.ref('users');

  /// Realtime Databaseへのデータ更新
  void write(WidgetRef ref) async {
    try {
      await dbRef.update({
        '$userID/count': ref.read(counterProvider),
      });
    } catch (e) {
      print('Error : $e');
    }
  }

  /// Realtime Databaseのデータ取得
  void read(WidgetRef ref) async {
    try {
      final snapshot = await dbRef.child(userID).get();
      if (snapshot.exists) {
        ref.read(counterProvider.notifier).state =
            snapshot.child('count').value as int;
      }
    } catch (e) {
      print('Error : $e');
    }
  }

  /// Realtime Databaseのデータ削除
  void remove() async {
    try {
      await dbRef.child(userID).remove();
    } catch (e) {
      print('Error : $e');
    }
  }
}

GitHubのページを貼ります。

github.com

Google Play and the Google Play logo are trademarks of Google LLC.