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のデータモデルはドキュメントやコレクションなどで構成されており、サポートされているデータの型はbool
、int
、Map
型に加え、日付や地理的な座標などもあります。
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のページを貼ります。