75
44

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 3 years have passed since last update.

FlutterAdvent Calendar 2018

Day 19

[Flutter] DartのclassとFirestoreデータを相互変換、そしてCRUDへ

Last updated at Posted at 2018-12-18

2021年12月追記

  • 今はfreezedを使って似たようなことをしています
  • document_id と 実際のデータのラッパークラスは記事と同様に使っています

元記事

Flutterに限らずシステム開発をしているとデータ構造を表すEntityクラスをDBにCRUDすることは多いと思います。FlutterではFirestoreのプラグインはありますが、Dartのカスタムクラスを引数にとるCRUDのメソッドはありません。つまり一度Mapに変換する必要があるということです。

本エントリではDartのクラスとMapを相互変換してFirestoreを使いやすくする方法について紹介します。

まずはシンプルに次のような構造のユーザーデータを使用する場合のクラスを考えます。

{
  nickname: "Bob",
  age: 23,
  isPushEnabled: true,
  createdAt: 2018-12-27 // Firestoreの日付型
}

エンティティ

User

例に示したユーザーデータを表すUserクラスを次のように定義しています。

class User {
  String name;
  int age;
  bool isPushEnabled;
  DateTime createdAt;

  // コンストラクタ
  User();

  // コンストラクタ
  // FirestoreのMapからインスタンス化
  User.fromMap(Map<String, dynamic> map) {
    this.name = map[UserField.name];
    this.age = map[UserField.age];
    this.isPushEnabled = map[UserField.isPushEnabled];

    // DartのDateに変換
    final originCreatedAt = map[UserField.createdAt];
    if (originCreatedAt is Timestamp) {
      this.createdAt = originCreatedAt.toDate();
    }
  }

  // Firestore用のMapに変換
  Map<String, dynamic> toMap() {
    return {
      UserField.name: this.name,
      UserField.age: this.age,
      UserField.isPushEnabled: this.isPushEnabled,
      UserField.createdAt: this.createdAt, // Dateはそのまま渡せる
    };
  }
}

UserクラスにはUser.fromMapというFirestoreのレスポンスのMapからインスタンスを生成するためのコンストラクタを定義しています。
また逆にFirestoreに登録するときに使用するためにtoMapというインスタンスのデータをMapに変換するための関数も定義しています。
この2つの関数を定義することでFirestoreのデータとDartのクラスの相互変換を可能にしています。

UserField

UserクラスにUserFieldというクラスが使用されていることに気づいた方もいると思います。
このクラスはFirestoreのフィールド名をタイポしないために定義したものです。

class UserField {
  static const name = "name";
  static const age = "age";
  static const isPushEnabled = "isPushEnabled";
  static const createdAt = "createdAt";
}

UserDocument

リレーショナルDBの場合IDは1つのフィールドに含まれているので、Userクラスのプロパティの一つとして組み込みやすいです。
Firestoreの場合データ自体にはIDは含まれていないので、同じUserクラスにまとめるのは少し違和感がありました。
しかしリストビューのアイテムを設定する時のように、IDとデータをセットで扱いたいことは多いです。
この問題を解決するためにIDとデータをセットにしたクラスをUserDocumentクラスとして別途定義しています。

class UserDocument {
  String documentId;
  User user;

  UserDocument(this.documentId, this.user);
}

データストア

CRUDのための機能を1箇所にまとめるために専用のクラスを以下のように作っています。

このクラスの関数に渡すのはDartのクラスであるUserクラスのインスタンスです。
このためFirestoreのデータ構造については関数の呼び出し元は特に意識しなくて良くなっています。
基本的なCRUDはaddUser getUser updateUser deleteUserです。
特定のフィールドのみ更新したい場合はupdateNameのように実装します。

class UserDatastore {
  static String getCollectionPath() {
    return "users";
  }

  static String getDocumentPath(String documentId) {
    return "users/$documentId";
  }

  static String addUser(User user) {
    final newDocument =
        Firestore.instance.collection(getCollectionPath()).document();
    newDocument.setData(user.toMap());
    return newDocument.documentID;
  }

  static Future<User> getUser(String documentId) async {
    final snapshot =
        await Firestore.instance.document(getDocumentPath(documentId)).get();
    if (snapshot.exists) {
      return User.fromMap(snapshot.data);
    } else {
      throw Error();
    }
  }

  static void deleteUser(String documentId) {
    Firestore.instance.document(getDocumentPath(documentId)).delete();
  }

  static void updateUser(String documentId, User user) {
    Firestore.instance
        .document(getDocumentPath(documentId))
        .setData(user.toMap());
  }

  static void updateName(String documentId, String name) {
    Firestore.instance
        .document(getDocumentPath(documentId))
        .updateData({UserField.name: name});
  }
}

Listenする系の関数についてはここに定義するかはちょっと迷っています。
なぜならListenの停止や再開などをウィジェット側とうまく分離できるか分からないからです。
現状の私のプロジェクトではListenする系のリクエストはウィジェットに書いてしまっています。

#全コード
使用したコードの全文をのせておきます。

エンティティ

entity/User.dart
import 'package:cloud_firestore/cloud_firestore.dart';

class User {
  String name;
  int age;
  bool isPushEnabled;
  DateTime createdAt;

  // コンストラクタ
  User();

  // コンストラクタ
  // FirestoreのMapからインスタンス化
  User.fromMap(Map<String, dynamic> map) {
    this.name = map[UserField.name];
    this.age = map[UserField.age];
    this.isPushEnabled = map[UserField.isPushEnabled];

    // DartのDateに変換
    final originCreatedAt = map[UserField.createdAt];
    if (originCreatedAt is Timestamp) {
      this.createdAt = originCreatedAt.toDate();
    }
  }

  // Firestore用のMapに変換
  Map<String, dynamic> toMap() {
    return {
      UserField.name: this.name,
      UserField.age: this.age,
      UserField.isPushEnabled: this.isPushEnabled,
      UserField.createdAt: this.createdAt, // Dateはそのまま渡せる
    };
  }
}

class UserField {
  static const name = "name";
  static const age = "age";
  static const isPushEnabled = "isPushEnabled";
  static const createdAt = "createdAt";
}

class UserDocument {
  String documentId;
  User user;

  UserDocument(this.documentId, this.user);
}

データストア

entity/UserDatastore.dart
import '../entity/User.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'dart:async';

class UserDatastore {
  static String getCollectionPath() {
    return "users";
  }

  static String getDocumentPath(String documentId) {
    return "users/$documentId";
  }

  static String addUser(User user) {
    final newDocument =
        Firestore.instance.collection(getCollectionPath()).document();
    newDocument.setData(user.toMap());
    return newDocument.documentID;
  }

  static Future<User> getUser(String documentId) async {
    final snapshot =
        await Firestore.instance.document(getDocumentPath(documentId)).get();
    if (snapshot.exists) {
      return User.fromMap(snapshot.data);
    } else {
      throw Error();
    }
  }

  static void deleteUser(String documentId) {
    Firestore.instance.document(getDocumentPath(documentId)).delete();
  }

  static void updateUser(String documentId, User user) {
    Firestore.instance
        .document(getDocumentPath(documentId))
        .setData(user.toMap());
  }

  static void updateName(String documentId, String name) {
    Firestore.instance
        .document(getDocumentPath(documentId))
        .updateData({UserField.name: name});
  }
}

エンティティとデータストアを使用するウィジェット

sample-app.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'dart:async';
import 'package:flutter/material.dart';
import './entity/User.dart';
import './datastore/UserDatastore.dart';

// 実験用
class SampleApp extends StatefulWidget {
  @override
  State createState() => SampleAppState();
}

class SampleAppState extends State<SampleApp> {
  UserDocument userDocument;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text("Flutter Firestore Example"),
        ),
        body: Column(
          children: <Widget>[
            Text(userDocument == null
                ? "No user"
                : "User Name: ${userDocument.user.name}"),
            FlatButton(
              child: Text("Create"),
              onPressed: () {
                final user = User();
                user.name = "Takashi";
                user.age = 36;
                user.isPushEnabled = false;
                user.createdAt = DateTime.now();
                final documentId = UserDatastore.addUser(user);
                print("added:${documentId}");
                setState(() {
                  userDocument = UserDocument(documentId, user);
                });
              },
            ),
            FlatButton(
              child: Text("Read"),
              onPressed: () async {
                final user =
                    await UserDatastore.getUser(userDocument.documentId);
                print("getUser success:${user.name}");
              },
            ),
            FlatButton(
              child: Text("Update"),
              onPressed: () {
                setState(() {
                  userDocument.user.name = "Jiro";
                  userDocument.user.age = 44;
                });
                UserDatastore.updateUser(
                    userDocument.documentId, userDocument.user);
              },
            ),
            FlatButton(
              child: Text("Delete"),
              onPressed: () {
                UserDatastore.deleteUser(userDocument.documentId);
                setState(() {
                  userDocument = null;
                });
              },
            ),
            FlatButton(
              child: Text("Update Name"),
              onPressed: () {
                setState(() {
                  userDocument.user.name = "Bob";
                });
                UserDatastore.updateName(userDocument.documentId, "Bob");
              },
            ),
          ],
        ),
      ),
    );
  }
}
75
44
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
75
44

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?