🔥

Cloud Firestore ODM

2024/07/16に公開

🔥Flutterで使える珍しいパッケージ

昔使ってみたことあったので、気になって使ってみた。
これ

公式サイトの情報はこれぐらいしかない?
https://firebase.flutter.dev/docs/firestore-odm/overview
https://github.com/FirebaseExtended/firestoreodm-flutter/blob/main/docs/code-generation.md

add package:

flutter pub add cloud_firestore_odm
flutter pub add json_annotation
flutter pub add --dev build_runner
flutter pub add --dev cloud_firestore_odm_generator
flutter pub add --dev json_serializable

🤔まだ使えるのか???

LIKES 107か...

公式の解説を翻訳すると...

モデルとは、Firestore上でどのようなデータを受信し、どのようなデータを変異させるかを表すものです。ODMは、すべてのデータがモデルに対して検証されることを保証し、モデルが有効でない場合はエラーがスローされます。

はじめに、Firestoreデータベース上に 「Users 」というコレクションがあるとします。このコレクションには、名前、年齢、電子メール(など)のようなユーザ情報を含む多くのドキュメントが含まれています。このデータのモデルを定義するために、クラスを作成します:

import 'package:json_annotation/json_annotation.dart';
import 'package:cloud_firestore_odm/cloud_firestore_odm.dart';

// This doesn't exist yet...! See "Next Steps"
part 'user.g.dart';

(explicitToJson: true)
class User {
  User({
    required this.name,
    required this.age,
    required this.email,
  });

  final String name;
  final int age;
  final String email;
}

🔨Creating references

モデル単体では何もしない。その代わりに、モデルを使って「参照」を作成します。参照によって、ODMはモデルを使用してFirestoreと対話できるようになります。

参照を作成するには、Firestoreデータベース内のコレクションへのポインタとして使用されるCollectionアノテーションを使用します。例えば、データベースのルートにあるusersコレクションは、以前に定義したUsersモデルに対応します:

(explicitToJson: true)
class User {
 // ...
}

<User>('users')
final usersRef = UserCollectionReference();

🤔ここまでまとめると

モデルを作る。モデルは何もしてくれない💦
コードを自動生成して、「参照」を作成する。

そもそもこのパッケージの良いところは何かというと、TimestampConverterを作らなくても良い。freezedの面倒臭いところをカバーしてくれる。ただし、Timestamp -> Datetime -> 日本時間に変換するのは自作する必要がある。intlを使えばできる。

使ってみるか...

ダミーのデータをこんな感じで用意する。

import 'package:json_annotation/json_annotation.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:cloud_firestore_odm/cloud_firestore_odm.dart';

// これはまだ存在しない...!次のステップ "を参照
part 'example.g.dart';

/// カスタムJsonSerializableアノテーション。
/// TimestampsやDateTimesのようなオブジェクトのデコードをサポートするカスタムJsonSerializableアノテーション。
/// この変数は、異なるモデル間で再利用することができます。
const firestoreSerializable = JsonSerializable(
  converters: firestoreJsonConverters,
  // 以下の値を `build.yaml` 内で設定することもできる。
  explicitToJson: true,
  createFieldMap: true,
  createPerFieldToJson: true,
);


class Example {
  Example({
    required this.title,
    required this.createdAt,
  });

  final String title;
  /// TimestampsやDateTimesのようなオブジェクトのデコードをサポートするカスタムJsonSerializableアノテーション。
  ()
  Timestamp? createdAt;
}
// このコードは、次のコマンドを実行することで生成されます。
// flutter pub run build_runner watch --delete-conflicting-outputs
<Example>('example')
final examplesRef = ExampleCollectionReference();

自動生成のコマンドを実行する

flutter pub run build_runner watch --delete-conflicting-outputs

UIに、Cloud Firestoreから取得したデータを表示する

取得したデータを表示するには、FirestoreBuilderとなるものを使う。querySnapshotだから、一度だけ全てのデータを取得するようだ。


<User>('users')
final usersRef = UserCollectionReference();

// ...

class UsersList extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return FirestoreBuilder<UserQuerySnapshot>(
      ref: usersRef,
      builder: (context, AsyncSnapshot<UserQuerySnapshot> snapshot, Widget? child) {
        if (snapshot.hasError) return Text('Something went wrong!');
        if (!snapshot.hasData) return Text('Loading users...');

        // Access the QuerySnapshot
        UserQuerySnapshot querySnapshot = snapshot.requireData;

        return ListView.builder(
          itemCount: querySnapshot.docs.length,
          itemBuilder: (context, index) {
            // Access the User instance
            User user = querySnapshot.docs[index].data;

            return Text('User name: ${user.name}, age ${user.age}');
          },
        );
      }
    );
  }
}

main.dartを編集して、こんな感じにすればデータを表示できます。

main.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:cloud_firestore_odm/cloud_firestore_odm.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:freezed_extention/example/example.dart';
import 'package:freezed_extention/firebase_options.dart';
import 'package:intl/intl.dart';

// yyyy-MM-dd HH:mm:ssで表示する拡張機能
// intlを使う
// extension type TimeConvert(Timestamp timestamp) {
//   String toTime() {
//     return DateFormat('yyyy-MM-dd HH:mm:ss').format(timestamp.toDate());
//   }
// }
// 拡張機能の定義を修正
extension TimeConvert on Timestamp {
  String toTime() {
    return DateFormat('yyyy-MM-dd HH:mm:ss').format(toDate());
  }
}

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

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(),
    );
  }
}

<Example>('example')
final examplesRef = ExampleCollectionReference();

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

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

class _MyHomePageState extends State<MyHomePage> {

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('Firebase Example'),
      ),
      body: FirestoreBuilder<ExampleQuerySnapshot>(
      ref: examplesRef,
      builder: (context, AsyncSnapshot<ExampleQuerySnapshot> snapshot, Widget? child) {
        if (snapshot.hasError) return Text('Something went wrong!');
        if (!snapshot.hasData) return Text('Loading users...');

        // Access the QuerySnapshot
        ExampleQuerySnapshot querySnapshot = snapshot.requireData;

        return ListView.builder(
          itemCount: querySnapshot.docs.length,
          itemBuilder: (context, index) {
            // Access the User instance
            Example example = querySnapshot.docs[index].data;

            return Text('title: ${example.title}, time ${example.createdAt!.toTime()}');
          },
        );
      }
    ),
    );
  }
}

こんな感じでございます。

感想

なかなか、面白い機能だが、riverpodのプロバイダーが使えなさそう。いい感じで使い分ければ便利なものになるかもしれない???

Discussion