FlutterアプリテンプレートのCloud Firestore利用部分のコードリーディング(hukusuke1007/flutter_app_template)

# はじめに

Flutter アプリテンプレート **hukusuke1007 / [flutter_app_template](https://github.com/hukusuke1007/flutter_app_template)** に Firebase の Cloud Firestore（NoSQL クラウド データベース）が使われている該当のコードを、公式ドキュメントと見比べて確認します。

## hukusuke1007 / [flutter_app_template](https://github.com/hukusuke1007/flutter_app_template) について

https://github.com/hukusuke1007/flutter_app_template

flutter_app_template は初めて使ってみているのですが、Flutter + Firebase アプリのスターターキットになっており、サンプル機能がとても充実しています。序盤はコードリーディングしつつ Flutter の開発手法を学び、個人開発に移っていこうと思っています。

## 目的

Flutter アプリテンプレート **hukusuke1007 / [flutter_app_template](https://github.com/hukusuke1007/flutter_app_template)** のコードで、タイムライン機能の Cloud Firestore 利用に関連するファイルを探して、公式ドキュメントと見比べながら、Cloud Firestore の理解を深めることを目的としています。

## 前提

- **hukusuke1007 / [flutter_app_template](https://github.com/hukusuke1007/flutter_app_template)** の導入（`git clone` から `flutter run` まで）が完了していること

https://qiita.com/ryamate/items/f50aa769dbc34a39fa64

- Cloud Firestore の導入が完了していること

https://qiita.com/ryamate/items/c2349525c682a214d20c

## 目次

[1. Firebase のプラグイン導入、初期化コードの確認](#1-firebase-のプラグイン導入初期化コードの確認)
[2. Cloud Firestore のインスタンス初期化・利用コードの確認](#2-cloud-firestore-のインスタンス初期化利用コードの確認)

# 1. Firebase のプラグイン導入、初期化コードの確認

導入しているプラグイン、初期化コードを確認します。

## 1-1. Firebase プラグイン導入

pubspec.yaml ファイルの依存関係に `cloud_firestore` プラグインがあります。

```yaml:確認： pubspec.yaml
...
dependencies:
  flutter:
    sdk: flutter
...
  # Firebase
  firebase_core: ^2.27.0
  firebase_analytics: ^10.8.9
  cloud_firestore: ^4.15.8
...
```

https://pub.dev/packages/cloud_firestore

## 1-2. Firebase 初期化

main.dart ファイルで Firebase を初期化しています。

```dart:確認： lib/main.dart
...

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  late final PackageInfo packageInfo;
  late final SharedPreferences sharedPreferences;
  late final Directory tempDirectory;
  Logger.configure();

  await (
    /// Firebase
    Firebase.initializeApp(),

...
}
```

ここで Firebase を初期化しています。`initializeApp()` メソッドは、Firebase プロジェクトの設定を読み込み、Firebase SDK を利用可能にします。これにより、アプリケーション内で Firebase の機能を使用できるようになり、Cloud Firestore も利用できるようになります。

# 2. Cloud Firestore のインスタンス初期化・利用コードの確認

続いて、Cloud Firestore のインスタンス初期化、データを追加するコードを確認します。

https://firebase.google.com/docs/firestore/manage-data/add-data?hl=ja#initialize

## 2-1. Cloud Firestore 初期化

Cloud Firestore のインスタンスを初期化するコードは、[公式ドキュメント](https://firebase.google.com/docs/firestore/manage-data/add-data?hl=ja#initialize) には以下のように書いてあります。

```dart
db = FirebaseFirestore.instance;
```

Flutter アプリテンプレートで、Cloud Firestore のインスタンスを初期化しているコードを確認します。

```dart:確認： lib/core/repositories/firestore/document_repository.dart
...

/// Riverpodプロバイダを使用して[DocumentRepository]を提供する。
///
/// FirebaseFirestoreのインスタンスを使用して、[DocumentRepository]を生成する。
/// - [DocumentRepositoryRef] : プロバイダの参照。
/// - Returns: [DocumentRepository]のインスタンス。
@Riverpod(keepAlive: true)
DocumentRepository documentRepository(DocumentRepositoryRef ref) {
  return DocumentRepository(FirebaseFirestore.instance);
}

/// Firestoreのドキュメントを管理するリポジトリクラス。
///
/// Firestoreのドキュメントに対する読み書き操作を簡素化し、
/// バッチ操作やトランザクションなどの機能も提供する。
class DocumentRepository {
  /// コンストラクタ。
  ///
  /// - [_firestore] : [FirebaseFirestore]のインスタンス。
  DocumentRepository(this._firestore);

  final FirebaseFirestore _firestore;

...
}
```

<details>
<summary>document_repository.dart 全文</summary>

```dart:lib/core/repositories/firestore/document_repository.dart(全文)
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import 'document.dart';

part 'document_repository.g.dart';

/// Riverpodプロバイダを使用して[DocumentRepository]を提供する。
///
/// FirebaseFirestoreのインスタンスを使用して、[DocumentRepository]を生成する。
/// - [DocumentRepositoryRef] : プロバイダの参照。
/// - Returns: [DocumentRepository]のインスタンス。
@Riverpod(keepAlive: true)
DocumentRepository documentRepository(DocumentRepositoryRef ref) {
  return DocumentRepository(FirebaseFirestore.instance);
}

/// Firestoreのドキュメントを管理するリポジトリクラス。
///
/// Firestoreのドキュメントに対する読み書き操作を簡素化し、
/// バッチ操作やトランザクションなどの機能も提供する。
class DocumentRepository {
  /// コンストラクタ。
  ///
  /// - [_firestore] : FirebaseFirestoreのインスタンス。
  DocumentRepository(this._firestore);

  final FirebaseFirestore _firestore;

  /// Firestoreのバッチ操作を取得する。
  ///
  /// - Returns: WriteBatchのインスタンス。
  WriteBatch get batch => _firestore.batch();

  /// 指定されたドキュメントパスにデータを保存する。
  ///
  /// - [documentPath] : ドキュメントのパス。
  /// - [data] : 保存するデータのマップ。
  Future<void> save(
    String documentPath, {
    required Map<String, dynamic> data,
  }) async {
    final doc = _firestore.doc(documentPath);
    await doc.set(
      data,
      SetOptions(merge: true),
    );
  }

  /// 指定されたドキュメントパスのデータを更新する。
  ///
  /// - [documentPath] : ドキュメントのパス。
  /// - [data] : 更新するデータのマップ。
  Future<void> update(
    String documentPath, {
    required Map<String, dynamic> data,
  }) async {
    final doc = _firestore.doc(documentPath);
    await doc.update(data);
  }

  /// 指定されたドキュメントパスのデータを取得する。
  ///
  /// - [documentPath] : ドキュメントのパス。
  /// - [source] : データの取得元（デフォルトはサーバーとキャッシュの両方）。
  /// - [fromCache] : キャッシュから取得したデータを処理するコールバック関数。
  /// - [decode] : データのデコード関数。
  /// - Returns: デコードされたドキュメントデータ。
  Future<Document<T>> fetch<T extends Object>(
    String documentPath, {
    Source source = Source.serverAndCache,
    void Function(Document<T>?)? fromCache,
    required T Function(Map<String, dynamic>) decode,
  }) async {
    if (fromCache != null) {
      try {
        final cache = await _firestore
            .doc(documentPath)
            .get(const GetOptions(source: Source.cache));
        fromCache(
          Document(
            ref: cache.reference,
            exists: cache.exists,
            entity: cache.exists ? decode(cache.data()!) : null,
          ),
        );
      } on Exception catch (_) {
        // ignore exception
        fromCache(null);
      }
    }

    final snap =
        await _firestore.doc(documentPath).get(GetOptions(source: source));
    return Document(
      ref: snap.reference,
      exists: snap.exists,
      entity: snap.exists ? decode(snap.data()!) : null,
    );
  }

  /// キャッシュから指定されたドキュメントパスのデータを取得する。
  ///
  /// - [documentPath] : ドキュメントのパス。
  /// - [decode] : データのデコード関数。
  /// - Returns: デコードされたドキュメントデータ。
  Future<Document<T>> fetchCache<T extends Object>(
    String documentPath, {
    required T Function(Map<String, dynamic>) decode,
  }) async {
    final doc = _firestore.doc(documentPath);
    try {
      final cache = await doc.get(const GetOptions(source: Source.cache));
      if (cache.exists) {
        return Document(
          ref: cache.reference,
          exists: cache.exists,
          entity: cache.exists ? decode(cache.data()!) : null,
        );
      }
    } on Exception catch (_) {
      // ignore exception
    }
    final snap = await doc.get(
      // ignore: avoid_redundant_argument_values
      const GetOptions(source: Source.serverAndCache),
    );
    return Document(
      ref: snap.reference,
      exists: snap.exists,
      entity: snap.exists ? decode(snap.data()!) : null,
    );
  }

  /// キャッシュのみから指定されたドキュメントパスのデータを取得する。
  ///
  /// - [documentPath] : ドキュメントのパス。
  /// - [decode] : データのデコード関数。
  /// - Returns: デコードされたドキュメントデータ。
  Future<Document<T>> fetchCacheOnly<T extends Object>(
    String documentPath, {
    required T? Function(Map<String, dynamic>) decode,
  }) async {
    final doc = _firestore.doc(documentPath);
    try {
      final cache = await doc.get(const GetOptions(source: Source.cache));
      if (cache.exists) {
        return Document(
          ref: cache.reference,
          exists: cache.exists,
          entity: cache.exists ? decode(cache.data()!) : null,
        );
      }
    } on Exception catch (_) {
      // ignore exception
    }
    return Document(
      ref: doc,
      entity: null,
      exists: false,
    );
  }

  /// 指定されたドキュメントパスのドキュメントが存在するかどうかを確認する。
  ///
  /// - [documentPath] : ドキュメントのパス。
  /// - Returns: ドキュメントが存在する場合はtrue、存在しない場合はfalse。
  Future<bool> exists(String documentPath) async {
    final doc = _firestore.doc(documentPath);
    // ignore: avoid_redundant_argument_values
    final snap = await doc.get(const GetOptions(source: Source.serverAndCache));
    return snap.exists;
  }

  /// キャッシュのみで指定されたドキュメントパスのドキュメントが存在するかどうかを確認する。
  ///
  /// - [documentPath] : ドキュメントのパス。
  /// - Returns: ドキュメントが存在する場合はtrue、存在しない場合はfalse。
  Future<bool> existsCache(String documentPath) async {
    final doc = _firestore.doc(documentPath);
    try {
      final cache = await doc.get(const GetOptions(source: Source.cache));
      return cache.exists;
    } on Exception catch (_) {
      // ignore exception
    }
    // ignore: avoid_redundant_argument_values
    final snap = await doc.get(const GetOptions(source: Source.serverAndCache));
    return snap.exists;
  }

  /// 指定されたドキュメントパスのドキュメントのスナップショットストリームを取得する。
  ///
  /// - [documentPath] : ドキュメントのパス。
  /// - Returns: ドキュメントスナップショットのストリーム。
  Stream<DocumentSnapshot<SnapType>> snapshots(String documentPath) =>
      _firestore.doc(documentPath).snapshots();

  /// 指定されたドキュメントパスのドキュメントを削除する。
  ///
  /// - [documentPath] : ドキュメントのパス。
  Future<void> remove(String documentPath) =>
      _firestore.doc(documentPath).delete();

  /// トランザクションを実行する。
  ///
  /// - [transactionHandler] : トランザクションの処理関数。
  /// - Returns: トランザクションの結果。
  Future<T> transaction<T>(TransactionHandler<T> transactionHandler) =>
      _firestore.runTransaction<T>(transactionHandler);
}

```    
</details>


この部分で `FirebaseFirestore.instance` が呼び出され、`DocumentRepository` クラスのインスタンスが生成されています。

※ドキュメンテーションコメントについては、アプリテンプレートに私が追記したものです

## 2-2. ドキュメントの作成または上書き

ドキュメントを作成または上書きするコードは、[公式ドキュメント](https://firebase.google.com/docs/firestore/manage-data/add-data?hl=ja#initialize) には以下のように書いてあります。

```dart
// Update one field, creating the document if it does not already exist.
final data = {"capital": true};

db.collection("cities").doc("BJ").set(data, SetOptions(merge: true));
```

※ ちなみに「ドキュメント」とは [Cloud Firestore のデータモデル](https://qiita.com/ryamate/items/c2349525c682a214d20c#cloud-firestore-%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E3%83%A2%E3%83%87%E3%83%AB) の用語です

> Cloud Firestore のデータモデルは、「コレクション」の中に「ドキュメント」があり、ドキュメントにデータ（フィールド、サブコレクション）があります。
```
📁 コレクション
  └── 📄 ドキュメント
        ├── フィールド
        └── フィールド
```

Flutter アプリテンプレートで、ドキュメントを作成または上書きするコードを確認します。

```dart:確認： lib/core/repositories/firestore/document_repository.dart
class DocumentRepository {
...
  
  /// 指定されたドキュメントパスにデータを保存する。
  ///
  /// - [documentPath] : ドキュメントのパス。
  /// - [data] : 保存するデータのマップ。
  Future<void> save(
    String documentPath, {
    required Map<String, dynamic> data,
  }) async {
    final doc = _firestore.doc(documentPath);
    await doc.set(
      data,
      SetOptions(merge: true),
    );
  }

...
}
```

`SetOptions(merge: true)` は、新しいデータを既存のドキュメントに統合するオプションで、ドキュメントが存在するかどうかわからない場合に、ドキュメント全体が上書きされないようにします。

ひとまず、データ作成という基本的なところまで確認できました。あとはコードを書いて慣れていこうと思います。

# おわりに

Flutter アプリテンプレート **hukusuke1007 / [flutter_app_template](https://github.com/hukusuke1007/flutter_app_template)** に Firebase の Cloud Firestore（NoSQL クラウド データベース）が使われている該当のコードを、公式ドキュメントと見比べて確認しました。

## やり残し

Flutter アプリテンプレートに関連して、以下を今後やっていこうと思います。

- Mason を使った Feature ディレクトリの作成など、README に書いてある開発方法の確認
- プラグインの確認。プラグインやコードがあるかもしれませんが、不要と分かった段階で削除しながら、開発を進めていこうと思います
- 参考文献の通読

ありがとうございました。