snovaのブログ

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

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

はじめに

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

今回はデータの保存を行うことができるCloud Firestoreについてです。

似たようなサービスに、写真や動画の保存に最適なCloud Storage、リアルタイムのクライアント間通信に最適なRealtime Databaseがあります。 公式サイトにRealtime Databaseとの比較がありますので、そのプロジェクトでどのデータベースを選択するかの参考になります。

目次

シリーズの内容

回数 内容 リンク
第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から、Firestore Databaseを選択し、データベースの作成を行います。 セキュリティ設定は「本番環境モード」、リージョンはasia-northeast1(東京)またはasia-northeast2(大阪)を選択します。

なお、Firebaseセキュリティルールとは、保管されたデータへのアクセスをどう許可するかを定義するための構文です。

基本的なセキュリティルールとして、認証済のすべてのユーザーがアクセス可能なテスト環境では、以下のように記述します。

service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if request.auth != null;
    }
  }
}

また、コンテンツ所有者のみがアクセスできる本番環境では、以下のように記述します。

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow only authenticated content owners access
    match /some_collection/{userId}/{documents=**} {
      allow read, write: if request.auth != null && request.auth.uid == userId
    }
  }
}

今回はユーザーIDの中にカウントされた数を記録するように、以下の通り設定しました。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId}/{documents=**} {
      allow read, write: if request.auth != null && request.auth.uid == userId
    }
  }
}

Firestoreのデータモデルドキュメントコレクションなどで構成されており、サポートされているデータの型boolintMap型に加え、日付や地理的な座標などもあります。

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

dependencies:
  cloud_firestore: ^3.1.15

Firestoreへのデータ追加、読取、削除の例を示します。 データの書き込みや読み込みにはRiverpodを使っています。

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

/// Firestoreのデータベース定義
final db = FirebaseFirestore.instance;

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

/// データ追加
void add(WidgetRef ref) {
  /// Map<String, dynamic>に変換
  final Map<String, dynamic> counterMap = {
    'count': ref.read(counterProvider),
  };

  /// Firestoreへデータ追加
  try {
    db.collection('users').doc(userID).set(counterMap);
  } catch (e) {
    print('Error : $e');
  }
}

/// データ取得
void get(WidgetRef ref) async {
  try {
    await db.collection('users').doc(userID).get().then(
      (event) {
        ref.read(counterProvider.notifier).state = event.get('count');
      },
    );
  } catch (e) {
    print('Error : $e');
  }
}

/// データ削除
void delete() async {
  try {
    db.collection('users').doc(userID).delete().then((doc) => null);
  } catch (e) {
    print('Error : $e');
  }
}

実際のカウント画面。

Firestoreにデータが入っているか確認します。

コード全文

前回からの変更点です。

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

main.dart

/// Flutter関係のインポート
import 'package:counter_firebase/firestore_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(
      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カウンターを開くためには認証してください。'),
        ],
      ),
    );
  }
}

/// ページ遷移ボタン
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,
      },
    );
  }
}

firestore_page.dart

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

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

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

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

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

class FirestorePageState extends ConsumerState<FirestorePage> {
  @override
  void initState() {
    super.initState();

    /// Firestoreの数値を読取
    FirestoreService().get(ref);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Firestoreカウンター'),
      ),
      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,
            ),

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

/// Firestoreの設定
class FirestoreService {
  /// firestoreのデータベース定義
  final db = FirebaseFirestore.instance;

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

  /// firestoreへのデータ更新
  void add(WidgetRef ref) {
    /// Map<String, dynamic>に変換
    final Map<String, dynamic> counterMap = {
      'count': ref.read(counterProvider),
    };

    /// Firestoreへデータ追加
    try {
      db.collection('users').doc(userID).set(counterMap);
    } catch (e) {
      print('Error : $e');
    }
  }

  /// firestoreのデータ取得
  void get(WidgetRef ref) async {
    try {
      await db.collection('users').doc(userID).get().then(
        (event) {
          ref.read(counterProvider.notifier).state = event.get('count');
        },
      );
    } catch (e) {
      print('Error : $e');
    }
  }

  /// firestoreのデータ削除
  void delete() async {
    try {
      db.collection('users').doc(userID).delete().then((doc) => null);
    } catch (e) {
      print('Error : $e');
    }
  }
}

GitHubのページを貼ります。

github.com

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