hamburger-tech-nits

主にプログラミングのNITSな話

FlutterのCloud Firestore ODMを活用して型安全なリクエストを実行する

Flutterで一般的な使い方でFirestoreにアクセスしようとすると、ドキュメント名やレスポンスを文字列でマッチングする必要があるため、型情報の復元のためのコードを色々書く必要がある。

Cloud Firestore ODMはその名の通りObject Document Mapperで、これを経由してFirestoreにアクセスすることで各種ドキュメントやフィールド情報を型安全に取得できる。今までStringのキーでアクセスするのが気持ち悪くて避けていたが、それが改善されそうなので使ってみた。

Cloud Firestore ODM は現在アルファ版です。今後大きな変更があるかもしれません。詳細はディスカッションを読むようアナウンスされています。

pub.dev

github.com

事前準備

まだFirebaseの準備ができていない場合は、ドキュメントをもとにflutterfireの設定する。

firebase.google.com

その後、アプリ起動時に初期化処理を呼び出す。

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

cloud_firestoreのインストール

firestoreを導入する。iOSは追加の設定があるので注意

firebase.flutter.dev

flutter pub add cloud_firestore
flutter run

cloud_firestore_odmのインストール

cloud_firestore_odmと一緒に、依存するライブラリも導入する。

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

モデル作成

JsonSerializable用の定義

const firestoreSerializable = JsonSerializable(
  converters: firestoreJsonConverters,
  // The following values could alternatively be set inside your `build.yaml`
  explicitToJson: true,
  createFieldMap: true,
);

モデルクラスを作成する。読みやすさのためにreferenceに対する参照はモデルクラスの上部に記載したが、build_runnerに失敗する場合は一度コメントアウトして実行する。 なお、ドキュメントではidの宣言に関する実装をoptionalのように表現されているが、個人的にはすべてのモデルで宣言していいと思う。あって困るものではないし。

part 'user.g.dart';

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

@firestoreSerializable
class User {
  User({
    required this.name,
    required this.age,
    required this.email,
    String? id,  // データ作成時はidを宣言しないので、optional引数にしている
  }) : id = id ?? '';


  @Id()  // この宣言があることで、ドキュメントのidをマッピングするコードが生成される。
  final String id;
  final String name;
  final int age;
  final String email;
}

定義が完了したら、build_runnerでファイル生成する。

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

データ生成

任意のタイミングでaddのメソッドを呼び出す。

  final doc = await usersRef.add(
                User(
                  name: "name1",
                  age: 1,
                  email: foobarbaz@yourdomain,
                ),
              );

データ呼び出し

ドキュメントではFirestoreBuilderでwidgerを生成する方法が紹介されている。

class User extends StatelessWidget {
  User(this.id);

  final String id;

  @override
  Widget build(BuildContext context) {
    return FirestoreBuilder<UserDocumentSnapshot>(
      // Access a specific document
      ref: usersRef.doc(id),
      builder: (context, AsyncSnapshot<UserDocumentSnapshot> snapshot, Widget? child) {
        if (snapshot.hasError) return Text('Something went wrong!');
        if (!snapshot.hasData) return Text('Loading user...');

        // Access the UserDocumentSnapshot
        UserDocumentSnapshot documentSnapshot = snapshot.requireData;

        if (!documentSnapshot.exists) {
          return Text('User does not exist.');
        }

        User user = documentSnapshot.data!;

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

サブコレクションと合わせてデータを整形するなど、細かい操作をしたい場合はFutureで受け取ったデータを使って自分でWidgetを組み立てるほうが楽かもしれない。

 usersRef.doc(widget.id!).get().then(
      (value) {
        setState(() {
          _user = value.data;
        });
      },
    );

まとめ

モデルのバリデーション方法やサブコレクションの連携方法も紹介されているので、気になる方はドキュメントを参照すること。2023/03/04時点で4ページしかなく、楽にキャッチアップできる。

github.com