【Flutter】Cloud Firestoreでデータの保存、取得

この記事は、Flutter大学 2021の 11日目 の記事です。
昨日は@ishihaya(Flutter大学)のFlutterでステージング環境やテスト環境へのデプロイを自動化 & 本番環境と共存させる【GitHub Actions x Firebase App Distributionの記事でした。

今日は僕です。

## はじめに

この記事はFlutterでCloud Firestoreでデータの保存、取得の実装方法を説明する記事です。
withConverterを使用しています。
withConverterを使うことで型を明示してコレクションやドキュメントを扱えます。

## 前提条件
- Cloud FirestoreをFirebaseの管理画面で使えるようになっていること
- Cloud Firestoreのルールで保存、取得ができること

※macを使用し、ライブラリ追加等行っています。

## 今回のCloud Firestoreのコレクション

今回はschoolsコレクションを仮で定義します。
schoolsコレクションで学校のマスターという意味で使用するものとします。
名前があり作成日時、更新日時、削除日時があるシンプルなものです。

## 実装方法
1. FirebaseのCloud Firestoreを使うライブラリの追加
1. Cloud Firestoreで扱うschoolモデルの作成
1. データの取得、保存部分の作成
1. サンプル作成

### 1.FirebaseのCloud Firestoreを使うライブラリの追加

ターミナルから以下のコマンドを実行し、ライブラリを追加
```flutter pub add cloud_firestore
```

### 2.Cloud Firestoreで扱うschoolモデルの作成

ポイント
1.Schoolモデルで使用するプロパティを宣言する
　final String name等ですね。
　コンストラクで必須のアノテーションも追加しています。
2.fromJsonの所でCloud Firestoreから使うデータをschoolオブジェクトに変換しています。
　createdAtはCloud FirestoreのTimestampからDartのDateTimeに変換します。
3.toJsonでCloud Firestoreに書き込む時にCloud Firestoreに合うように変換します。
　DartとFirebaseで型が違うときはここで合わせます。

fromJSONは名前付きコンストラクタです。


```school.dart

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/foundation.dart';

@immutable
class School {
  final String name;
  final DateTime createdAt;
  final DateTime updatedAt;
  final DateTime? deletedAt;

  School({
    required this.name,
    required this.createdAt,
    required this.updatedAt,
    this.deletedAt,
  });

  //Firebaseからデータを取得する際の変換処理
  School.fromJson(Map<String, Object?> json)
      : this(
            name: json['name']! as String,
            createdAt: (json['createdAt']! as Timestamp).toDate() as DateTime,
            updatedAt: (json['updatedAt']! as Timestamp).toDate() as DateTime,
            deletedAt:
                (json['deletedAt'] as Timestamp?)?.toDate() as DateTime?);

  //DartのオブジェクトからFirebaseへ渡す際の変換処理
  Map<String, Object?> toJson() {
    Timestamp? deletedTimestamp;
    if (deletedAt != null) {
      deletedTimestamp = Timestamp.fromDate(deletedAt!);
    }
    return {
      'name': name,
      'createdAt': Timestamp.fromDate(createdAt), //DartのDateTimeからFirebaseのTimestampへ変換
      'updatedAt': Timestamp.fromDate(updatedAt), //DartのDateTimeからFirebaseのTimestampへ変換
      'deletedAt': deletedTimestamp
    };
  }
}

```

### 3.データの取得、保存部分の作成

ポイント
withConverter<>にSchoolモデルを型として指定します。
あとは
Firestoreから取得する時にはfromJsonを設定し、
Firestoreへ保存するときはtoJsonを設定します。
そして、Firestoreのdocsでドキュメントのリストを返します。


```schoolRepository.dart
///
/// 学校を扱うリポジトリ
///
class SchoolRepository {
  final schoolsManager = FirebaseFirestore.instance.collection('schools');

  ///
  /// 学校情報を取得する
  ///
  Future<List<QueryDocumentSnapshot<School>>> getSchools() async {
    final schoolRef = schoolsManager.withConverter<School>(
        fromFirestore: (snapshot, _) => School.fromJson(snapshot.data()!),
        toFirestore: (school, _) => school.toJson());
    final schoolSnapshot = await schoolRef.get();
    return schoolSnapshot.docs;
  }

  ///
  /// 学校情報を保存する
  ///
  Future<String> insert(School school) async {
    final data = await schoolsManager.add(school.toJson());
    return data.id;
  }
  
}



```

withConverterのソースを見るとコメントで分かりやすく記載されているので、見た方がより理解が深まると思います。

```dart
  /// Transforms a [CollectionReference] to manipulate a custom object instead
  /// of a `Map<String, dynamic>`.
  ///
  /// This makes both read and write operations type-safe.
  ///
  /// ```dart
  /// final modelsRef = FirebaseFirestore
  ///     .instance
  ///     .collection('models')
  ///     .withConverter<Model>(
  ///       fromFirestore: (snapshot, _) => Model.fromJson(snapshot.data()!),
  ///       toFirestore: (model, _) => model.toJson(),
  ///     );
  ///
  /// Future<void> main() async {
  ///   // Writes now take a Model as parameter instead of a Map
  ///   await modelsRef.add(Model());
  ///
  ///   // Reads now return a Model instead of a Map
  ///   final Model model = await modelsRef.doc('123').get().then((s) => s.data());
  /// }
  /// ```
  // `extends Object?` so that type inference defaults to `Object?` instead of `dynamic`
  @override
  CollectionReference<R> withConverter<R extends Object?>({
    required FromFirestore<R> fromFirestore,
    required ToFirestore<R> toFirestore,
  });
```

### 4.サンプル
```sample.dart
    () async {

      final schoolRepository = SchoolRepository();
      //データの取得のサンプル
      final schools = await schoolRepository.getSchools();
      for (var school in schools) {
        print("ドキュメントID:" + school.id.toString());
        print("学校名" + school.data().name);
        print("作成日時:" + school.data().createdAt.toString());
      }

      //インサートのサンプル
      final now = DateTime.now();
      final insertSchool = new School(
        name:"サンプル学校",
        updatedAt: now,
        createdAt: now
      );
      //ドキュメントIDを取得
      final documentId = await schoolRepository.insert(insertSchool);
      print("ドキュメントID:" + documentId.toString());

      final insertDeletedSchool = new School(
          name:"サンプル削除学校",
          updatedAt: now,
          createdAt: now,
          deletedAt: now
      );
      //ドキュメントIDを取得
      final deletedDocumentId = await schoolRepository.insert(insertDeletedSchool);
      print("ドキュメントID:" + deletedDocumentId.toString());
    }();
```

### 参考リンク
・公式サイト
https://pub.dev/packages/cloud_firestore/install