【Flutter】Cloud Firestoreと連携

flutter

1. 概要

FlutterとCloud Firestoreを連携し、データ管理をする内容となります。

対象としては開発を1年程やってて自分で最初から開発してみたい方になります。そのため細かい用語などの説明はしません。

2. プロジェクトの準備

2-1. プロジェクトを作成

※未だNodeがインストールされてない場合はこちらを参考

3. Firestoreのセットアップ

こちらを参考

4. プロジェクトのロケーションを変更

こちらを参考

5. firebase-toolsのインストール

npm install -g firebase-tools

firebase --version

6. FlutterFireの初期化

6-1. flutterfire_cli

dart pub global activate flutterfire_cli

6-2. configure

  • 対象のFirebaseのプロジェクトを選択
flutterfire configure

※flutterプロジェクトに「lib/firebase_options.dart」が生成される。

7. Dependenciesの追加

7-1. FlutterからFirebaseへの接続

flutter pub add firebase_core

7-2. Cloud Firestore Plugin for Flutter

flutter pub add cloud_firestore

8. ファイルの修正

8-1. 「android/local.properties」に一行追記

flutter.minSdkVersion=23

8-2. 「android/app/build.gradle」を修正

    defaultConfig {
        applicationId "com.example.firestore_demo"
        // minSdkVersion flutter.minSdkVersion // 修正前
        minSdkVersion localProperties.getProperty('flutter.minSdkVersion') // 修正後
        targetSdkVersion flutter.targetSdkVersion
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
    }

9. データ構造

{
  users: {
    {userId}: {
      "userId": string,
      "name": string,
      "createdAt": timestamp,
      bookings: {
        {bookingId}: {
          "place": string,
          "price": int,
          "createdAt": timestamp
        }
      }
    }
  }
}

10. ソースコード

10-1. lib/firebase_options.dart

自動生成

10-2. lib/models/booking.dart

import 'package:cloud_firestore/cloud_firestore.dart';

class Booking {
  final String place;
  final int price;
  final DateTime createdAt;

  Booking({required this.place, required this.price, required this.createdAt});

  factory Booking.fromFirestore(DocumentSnapshot<Map<String, dynamic>> snapshot,
      SnapshotOptions? options) {
    final data = snapshot.data();
    return Booking(
        place: data?['place'],
        price: data?['price'],
        createdAt: data?['createdAt'].toDate());
  }

  Map<String, dynamic> toFirestore() {
    return {
      "place": place,
      "price": price,
      "createdAt": createdAt,
    };
  }
}

10-3. lib/db/firestore_helper.dart

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firestore_demo/models/booking.dart';

class FirestoreHelper {
  FirestoreHelper._getInstance();
  static final FirestoreHelper instance = FirestoreHelper._getInstance();

  final _db = FirebaseFirestore.instance;

  static const _collectionUsers = 'users';
  static const _subCollectionBookings = 'bookings';

  Future<List<DocumentSnapshot>> selectAllBookings(String userId) async {
    final collectionRef = _db
        .collection(_collectionUsers)
        .doc(userId)
        .collection(_subCollectionBookings)
        .withConverter(
          fromFirestore: Booking.fromFirestore,
          toFirestore: (Booking bookings, _) => bookings.toFirestore(),
        );
    final collectionSnap = await collectionRef.get();
    return collectionSnap.docs;
  }

  Future<Booking?> selectBooking(String userId, String place) async {
    final docRef = _db
        .collection(_collectionUsers)
        .doc(userId)
        .collection(_subCollectionBookings)
        .doc(place)
        .withConverter(
            fromFirestore: Booking.fromFirestore,
            toFirestore: (Booking booking, _) => booking.toFirestore());
    final docSnap = await docRef.get();
    return docSnap.data();
  }

  Future<void> insert(Booking booking, String userId) async {
    final docRef = _db
        .collection(_collectionUsers)
        .doc(userId)
        .collection(_subCollectionBookings)
        .doc(booking.place)
        .withConverter(
            fromFirestore: Booking.fromFirestore,
            toFirestore: (Booking booking, _) => booking.toFirestore());
    return await docRef.set(booking);
  }

  Future<void> delete(String userId, String docId) async {
    return await _db
        .collection(_collectionUsers)
        .doc(userId)
        .collection(_subCollectionBookings)
        .doc(docId)
        .delete();
  }
}

10-4. lib/views/home.dart

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firestore_demo/db/firestore_helper.dart';
import 'package:firestore_demo/models/booking.dart';
import 'package:flutter/material.dart';

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final String _userId = 'test';
  final TextEditingController _controllerPlace = TextEditingController();
  final TextEditingController _controllerPrice = TextEditingController();
  List<Booking> _bookingList = [];

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

  @override
  void dispose() {
    _controllerPlace.dispose();
    _controllerPrice.dispose();
    super.dispose();
  }

  List<Booking> _bookingListFromDocToList(
      List<DocumentSnapshot> bookingSnapshot) {
    return bookingSnapshot
        .map((doc) => Booking(
            place: doc['place'],
            price: doc['price'],
            createdAt: doc['createdAt'].toDate()))
        .toList();
  }

  void _getBookingList() async {
    List<DocumentSnapshot> bookingSnapshot =
        await FirestoreHelper.instance.selectAllBookings(_userId);
    _bookingList = _bookingListFromDocToList(bookingSnapshot);
    setState(() {});
  }

  void _addBooking(String place, String price) async {
    final Booking booking = Booking(
        place: place, price: int.parse(price), createdAt: DateTime.now());
    await FirestoreHelper.instance.insert(booking, _userId);
    _bookingList.add(booking);
    setState(() {});
  }

  void _deleteBooking(String docId, int index) async {
    await FirestoreHelper.instance.delete(_userId, docId);
    _bookingList.removeAt(index);
    setState(() {});
  }

  void _getBooking(String docId) async {
    Booking? booking =
        await FirestoreHelper.instance.selectBooking(_userId, docId);
    if (booking != null) {
      // ignore: use_build_context_synchronously
      ScaffoldMessenger.of(context).showSnackBar(SnackBar(
        content: Text('${booking.place} : ${booking.price}'),
        duration: const Duration(seconds: 3),
      ));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: SafeArea(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Expanded(
              child: SizedBox(
                child: _bookingList.isEmpty
                    ? const Center(child: Text('NO DATA'))
                    : ListView.builder(
                        itemCount: _bookingList.length,
                        itemBuilder: (BuildContext context, int index) {
                          final booking = _bookingList[index];
                          return Card(
                            child: InkWell(
                              child: Padding(
                                padding: const EdgeInsets.all(8.0),
                                child: Row(
                                  children: <Widget>[
                                    Expanded(
                                      child: Text(
                                        booking.place,
                                        style: const TextStyle(fontSize: 20),
                                      ),
                                    ),
                                    Expanded(
                                      child: Text(
                                        booking.price.toString(),
                                        style: const TextStyle(fontSize: 20),
                                      ),
                                    ),
                                    SizedBox(
                                      width: 50,
                                      height: 25,
                                      child: ElevatedButton(
                                          onPressed: () {
                                            _getBooking(booking.place);
                                          },
                                          child: const Icon(
                                            Icons.book,
                                            size: 15,
                                          )),
                                    ),
                                    const SizedBox(
                                      width: 5,
                                      height: 25,
                                    ),
                                    SizedBox(
                                      width: 50,
                                      height: 25,
                                      child: ElevatedButton(
                                          onPressed: () {
                                            _deleteBooking(
                                                booking.place, index);
                                          },
                                          child: const Icon(
                                            Icons.delete,
                                            size: 15,
                                          )),
                                    ),
                                  ],
                                ),
                              ),
                            ),
                          );
                        }),
              ),
            ),
            Row(
              children: [
                Expanded(
                    child: TextField(
                  controller: _controllerPlace,
                  autofocus: false,
                  decoration: const InputDecoration(labelText: 'Tokyo'),
                )),
                Expanded(
                    child: TextField(
                  controller: _controllerPrice,
                  autofocus: false,
                  decoration: const InputDecoration(labelText: '10000'),
                  onSubmitted: (String value) {
                    _addBooking(_controllerPlace.text, value);
                    _controllerPlace.clear();
                    _controllerPrice.clear();
                  },
                )),
                ElevatedButton(
                    onPressed: () {
                      _addBooking(_controllerPlace.text, _controllerPrice.text);
                      _controllerPlace.clear();
                      _controllerPrice.clear();
                    },
                    child: const Icon(Icons.add)),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

10-5. lib/main.dart

import 'package:firebase_core/firebase_core.dart';
import 'package:firestore_demo/firebase_options.dart';
import 'package:firestore_demo/views/home.dart';
import 'package:flutter/material.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Bookings',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'My bookings'),
    );
  }
}

11. 結果

12. 備考

FlutterからFirestoreにアクセスしてデータのやり取りをしてみました。

  • 登録
  • 一覧取得
  • 1件取得
  • 削除

13. 参考

関連記事

  1. flutter

    【Flutter】2回目以降のリリース,Android,iOS

  2. flutter

    【Flutter】画面をキャプチャし画像として保存

  3. Firebase

    【Firebase】Local Emulator Suiteを起動する…

  4. 【Widget of the Week】#1 Safe Area

  5. flutter

    【Flutter】Demoプロジェクトの値の状態を管理(Flutter…

  6. flutter

    【Flutter】Flutter実装メモ  enumの拡張

最近の記事

  1. AWS
  2. AWS
  3. AWS
  4. AWS
  5. AWS
  6. AWS
  7. AWS
  8. AWS

制作実績一覧

  1. Checkeys