36
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Flutter大学Advent Calendar 2021

Day 18

期待大!FlutterFire の Cloud Firestore ODM を触ってみた

Last updated at Posted at 2021-12-18

はじめに

日本時間の 2021 年 12 月 8 日 に Flutter SDK のバージョン 2.8 の安定版のリリースと同時に、Cloud Firestore ODM も発表されました!🙌

紹介記事を読みながら感銘を受け、これは期待大だと確認して、早速かんたんに触ってみたので 2021 年末のアドベントカレンダーの記事としてまとめます。

記事を執筆している 2021 年 12 月 16 日時点で、まだアルファ版ということで不具合の報告が確認されたり、実装予定・未実装の機能なども多くあるようなので、この記事をお読みの時期と合わせてご注意ください。Cloud Firestore ODM に大きな変更があれば、できるだけ追随して記事更新していこうと思います。

公式の各種情報は、次のようなリンクを参考にしてください。

また、この記事の内容は、記事を書く数日前に、私もコミュニティのメンバーとして参加しているFlutter 大学で、中心として毎週水曜日の夜に定期開催させて頂いている共同勉強会の題材としても取り上げました!

Flutter エンジニアはもちろん、エンジニアでない方も、これから学習していく方も参加している、リアルな繋がりも含めて、活発でとても雰囲気の良いコミュニティですのでご興味のある方は覗いてみてください。

その際使用した資料は次のようなリンクです。

Cloud Firestore ODM の概要

Cloud Firestore ODM はその名の通り、Cloud Firestore の Object/Document Mapper として機能します。

FlutterFire(この文脈では Flutter x Firestore)のアプリ開発を、型安全にボイラープレートコードを減らして、より快適に行うことを目指しているように思われます。

Object/Document Mapper (ODM) として機能するというのは、Firestore のドキュメントと Dart クラス(データクラス・オブジェクト)を紐付けるコードの自動生成ができるということで、決められた形式の少ない記述量で、ドキュメント ↔ Dart クラス間のシリアライズ・デシリアライズ (fromFirestore, toFirestore) の処理を含む便利なクラスやメソッドが自動生成されます。

ざっくり言うと、下記のようにトップレベルの /users コレクションのドキュメントを意味する User クラスを定義するだけで、ユーザーコレクション・ドキュメントに対する各種の Read/Write のメソッドが生成されるのに加え、すでに Dart の User クラスに紐付いた UserCollectionReferenceUserDocumentReference という型安全なコレクション・ドキュメントのレファレンスが生成されます。

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

part 'user.g.dart';

@JsonSerializable()
class User {
  User({
    required this.name,
    required this.age,
    required this.email,
  });

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

@Collection<User>('users')
final usersRef = UserCollectionReference();
UserDocumentReference userRef({required String userId}) =>
    UserDocumentReference(usersRef.doc(userId).reference);

さらに、FirestoreBuilder というウィジェットも使用可能になるので、下記のように ref プロパティに User 型に紐付いた UserCollection である usersRef を指定するだけで、Firestore の /users コレクションのドキュメント一覧をリアルタイム取得するコードを型安全に完結に書くことができてしまいます。

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

class UsersList extends StatelessWidget {
  @override
  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...');
        UserQuerySnapshot querySnapshot = snapshot.requireData;
        return ListView.builder(
          itemCount: querySnapshot.docs.length,
          itemBuilder: (context, index) {
            User user = querySnapshot.docs[index].data;
            return Text('User name: ${user.name}, age ${user.age}');
          },
        );
      }
    );
  }
}

つまり、Firestore DOM とは(またはそれが今後目指しているのは)ざっくり言うと、いままで

  • json_serializable や freezed を使ってデータクラスと fromFirestore, toFirestore メソッドを定義していた
  • それらを withConverter と組み合わせて型安全な Collection, Document のレファレンスにしていた
  • FutureBuilderStreamBuilder を使って Firestore のデータを画面に表示していた

のを、全てまとめて一緒くたに行えるようなパッケージだと言えそうです!(※ただしこの記事を執筆している 2021 年 12 月 16 日時点ではアルファ版であり、freezed との連携はできないことや、その他の制約や今後実装予定・未実装の機能が多くある点にはご注意ください)。

ただでさえクライアントとサーバの垣根が低い開発環境だと言える FlutterFire なアプリが、さらにシームレスに、ウィジェットのすぐ背後に Firestore のコレクション・ドキュメントが透けて見えるような世界観に感銘を受けました。

スプレッドシートや Figma のデザインデータからアプリを素早く作れます、といった文脈のサービスは今までも見かけていましたし、FlutterFire なアプリ開発との親和性の高い非常に興味深い取り組みだなと思っていましたが、今後は Firestore ODM も使用することで、私たちは Firestore のドキュメント (JSON) を単に並べて色を付けることには労力を割くことなく、yaml や json のようなドキュメントに決められた形式でスキーマ定義・データモデリングさえしてしまえば、アプリの基本的な画面や機能が自動で生成される、などという想像もできてしまいます。

Firestore ODM を使わずに似たことを実現すると

比較のために Firestore ODM を使用せずに型安全を保ちながら FlutterFire なアプリを開発するときを考えてみましょう。

たとえば freezed を使用して次のように User クラスを定義しするでしょう。

import 'package:freezed_annotation/freezed_annotation.dart';

part 'user.freezed.dart';
part 'user.g.dart';

@freezed
class User with _$User {
  const factory User({
    required String name,
    required int age,
  }) = _User;

  factory User.fromJson(Map<String, dynamic> json) =>
      _$UserFromJson(json);

  factory User.fromDocumentSnapshot(DocumentSnapshot documentSnapshot) {
    final data = documentSnapshot.data()! as Map<String, dynamic>;
    return User.fromJson(data);
  }
}

そして、/users コレクションからユーザー一覧を取得する際には、withConverter を使うことで型安全を保ち、次のようなコードを書きます。

Stream<List<User>> usersStream() {
  final snapshots = FirebaseFirestore.instance.collection('users')
      .withConverter<User>(
        fromFirestore: (snapshot, _) => User.fromDocumentSnapshot(snapshot),
        toFirestore: (obj, _) => obj.toJson(),
      )
      .snapshots();
  final result = snapshots.map((qs) => qs.docs.map((qds) => qds.data()).toList());
  return result;
}

このユーザー一覧を StreamBuilder を用いて画面に表示するなら次のようになるでしょう。

class UsersList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StreamBuilder<User>(
      ref: usersStream(),
      builder: (context, snapshot) {
        if (snapshot.hasError) return Text('Something went wrong!');
        if (!snapshot.hasData) return Text('Loading users...');
        final querySnapshot = snapshot.requireData;
        return ListView.builder(
          itemCount: querySnapshot.docs.length,
          itemBuilder: (context, index) {
            User user = querySnapshot.docs[index].data;
            return Text('User name: ${user.name}, age ${user.age}');
          },
        );
      }
    );
  }
}

さらに例えばユーザーを年齢の降順に並び替えたければ、

Stream<List<User>> usersStreamOrderByAge() {
  final snapshots = FirebaseFirestore.instance.collection('users')
      .withConverter<User>(
        fromFirestore: (snapshot, _) => User.fromDocumentSnapshot(snapshot),
        toFirestore: (obj, _) => obj.toJson(),
      )
      .orderBy('age')
      .snapshots();
  final result = snapshots.map((qs) => qs.docs.map((qds) => qds.data()).toList());
  return result;
}

これに相当するコードを書く必要があります。

もちろん上記の通りでもそれなりにスマートに、型安全を保ちながら Firestore からのドキュメントの読み込みとウィジェットでの表示ができているようにも思えます。

Firestore ODM を用いると

一方、やや繰り返しの説明となりますが、Firestore ODM による型安全なコレクション・ドキュメントレファレンスの自動生成と FirestoreBuilder を使用すると、withConverterStreamBuilderFutureBuilder を自分自身で書いていた箇所を省くことができます。さらに自動で whereAge
orderByAge のような where 句、order by 句の条件も生成されるので、それらも含めて型安全に記述量少なく同じことを実現することができます。

クラス定義の手間はそれほど変わりません。

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

part 'user.g.dart';

@JsonSerializable()
class User {
  User({
    required this.name,
    required this.age,
    required this.email,
  });

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

@Collection<User>('users')
final usersRef = UserCollectionReference();
UserDocumentReference userRef({required String userId}) =>
    UserDocumentReference(usersRef.doc(userId).reference);

FirestoreBuilder を使えば StreamBuilderwithConverter のことは意識する必要がなくなります。せっかくなので orderByAge()FirestoreBuilderref の部分につけておきました。

class UsersList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FirestoreBuilder<UserQuerySnapshot>(
      ref: usersRef.orderByAge(),
      builder: (context, AsyncSnapshot<UserQuerySnapshot> snapshot, Widget? child) {
        if (snapshot.hasError) return Text('Something went wrong!');
        if (!snapshot.hasData) return Text('Loading users...');
        UserQuerySnapshot querySnapshot = snapshot.requireData;
        return ListView.builder(
          itemCount: querySnapshot.docs.length,
          itemBuilder: (context, index) {
            User user = querySnapshot.docs[index].data;
            return Text('User name: ${user.name}, age ${user.age}');
          },
        );
      }
    );
  }
}

さらに、FirestoreBuilder クラスの内部を覗いてみると、そのコンストラクタの上のコメントに次のようなことが書いてあります。

This is a better solution than [StreamBuilder] for listening a Firestore
reference, as [FirestoreBuilder] will cache the stream created with ref.snapshots,
which could otherwise result in a billable operation.

つまり、キャッシュを利用した低コストな読み込みという観点で、FirestoreBuilder の方が優れているようです。

まとめ

以上の説明で、FlutterFire なアプリを開発する仲間であるみなさんと、Firestore ODM でできることやその魅力を共有できていれば嬉しいです!もしよろしければ SNS 等でも繋がり情報交換しましょう🧑‍💻

型安全で高効率、開発者体験もグンと上がっていくことになりそうな Firestore ODM の今後の発展を、同時期に発表された FlutterFire UI などとも一緒に追いかけていければ、そして私も何かしらの形でそのような OSS のコミュニティに貢献していければ良いな、と思っています🚀

最後までお読み下さりありがとうございました!!


その他の投稿の紹介:

36
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
36
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?