Flutter Tips💡 – FirestoreでDateTime型データを扱う際の注意

Firestoreを使用する際、以前少し詰まってしまった問題があったので備忘録として。

何が問題…?

アプリを作る際、日付データをFirestoreへ保存したいときがありますよね!

例えば

  • データを作成した日時を保存したい
  • ユーザの最終ログイン日時を保存したい

などなど・・・。

しかし、FirestoreではDateTime型をやり取りする際は、少し注意が必要です。

なぜかというと、Firebaseでは「Timestamp型」という標準のDartにはない型を使用しているからです。

Timestamp class – cloud_firestore_platform_interface library – Dart API

じゃあ、具体的にどういう時に問題が発生するのってところを見ていきます。

例:日付を含むデータをリスト表示するアプリ

まずこのようなデータクラスを用意しました。

class DateData {
  final String id;
  final DateTime date;

  const DateData({
    @required this.id,
    @required this.date,
  });

  // [変換] Map<String, dynamic> -> DateData
  factory DateData.fromMap(Map<String, dynamic> map) {
    return new DateData(
      id: map['id'] as String,
      date: map['date'] as DateTime,
    );
  }

  // [変換] DateData -> Map<String, dynamic>
  Map<String, dynamic> toMap() {
    // ignore: unnecessary_cast
    return {
      'id': this.id,
      'date': this.date,
    } as Map<String, dynamic>;
  }
}

「ID情報」と「日時情報」を持つシンプルなクラスです。

また、Firebaseとデータをやり取りするための相互型変換メソッドがあります。

次は保存したデータをリスト表示する画面を見てみましょう。

こっちはそんなに重要じゃないのでサラッと流し読みでOKです。

class HomeScreen extends StatefulWidget {
  @override
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {

  List<DateData> _dataList;

  bool _isLoading;

  @override
  void initState() {
    super.initState();
    _dataList = [];
    _isLoading = false;

    Future(() => _getAllData());
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('日付データの変換'),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: _addData,
      ),
      body: _isLoading
          ? Center(child: CircularProgressIndicator())
          : ListView.builder(
              itemCount: _dataList.length,
              itemBuilder: (context, index) {
                final data = _dataList[index];
                return ListTile(
                  title: Text('日時: ${data.date}'),
                  onLongPress: () => _deleteData(data.id),
                );
              },
            ),
    );
  }

  // 保存先コレクションのリファレンス
  CollectionReference get _cRef =>
      FirebaseFirestore.instance.collection('data');

  // [取得] 全データ取得
  Future<void> _getAllData() async {
    // ①ローディングインジケータを表示
    setState(() => _isLoading = true);

    // ②Firestoreから全てのデータを取得
    // ③取得した各Map型データをDateData型に変換
    // ④_dateListに入れる
    _dataList = await _cRef.get().then((snapshot) {
      return snapshot.docs
          .map<DateData>((doc) => DateData.fromMap(doc.data()))
          .toList();
    });

    // ⑤ローディングインジケータを非表示
    setState(() => _isLoading = false);
  }

  // [追加] 現在の日時データを追加
  Future<void> _addData() {
    // ①新しいDateDataを作成
    final newData = DateData(
      id: Uuid().v1(),
      date: DateTime.now(),
    );

    // ②DateData -> Map型に変換して保存
    // ③リストを更新
    return _cRef
        .doc(newData.id)
        .set(newData.toMap())
        .then((_) => _getAllData());
  }

  // [削除] 長押ししたデータを削除
  Future<void> _deleteData(String id) {
    // ①指定IDのデータを削除
    // ②リストを更新
    return _cRef.doc(id).delete().then((_) => _getAllData());
  }
}

これもシンプルですね。

画面上の「+」ボタンを押すと、押したときの日時情報がFirebaseへ保存され、

それらをリスト形式で表示するという画面です。

※一応削除機能もつけましたが今回は特に関係ないですw

Firebaseから取得したデータはMap<String, dynamic>形式なので、先程の変換メソッドを使ってDateData型へ変換しています。 (保存時は逆)

画面でけた

キャスト例外が発生

では「+」ボタンを押して、データを保存してみます。

type ‘Timestamp’ is not a subtype of type ‘DateTime’ in type cast 例外が発生!

Firestoreから取得したデータをDateTime型にキャストした際、

「このデータはTimestamp型だからDateTime型にはできないですよ〜」

と言われてしまったわけです。

気になった方もいると思いますが、DateTime型でも「保存はできる」みたいです。

ここは内部でうまいことやってくれているんですね、ナイス〜!

class DateData{
  ...

  // [変換] Map<String, dynamic> -> DateData
  factory DateData.fromMap(Map<String, dynamic> map) {
    return new DateData(
      id: map['id'] as String,
      date: map['date'] as DateTime, // <- コレがNG!
    );

  // [変換] DateData -> Map<String, dynamic>
  Map<String, dynamic> toMap() {
    // ignore: unnecessary_cast
    return {
      'id': this.id,
      'date': this.date, // <- これはOK!
    } as Map<String, dynamic>;
  }
  ...
  }
ちゃんと保存されてる

とすれば、取得時の変換処理を見直せばいいですね。

対処法:Timestampクラスの変換メソッドを使う

class DateData{
  ...

  // [変換] Map<String, dynamic> -> DateData
  factory DateData.fromMap(Map<String, dynamic> map) {
    return new DateData(
      id: map['id'] as String,
      // date: map['date'] as DateTime,
      date: (map['date'] as Timestamp)?.toDate(), // <- Timestamp.toDate()を使う
    );
  }

  ...
}

対処法は、

  • Timestamp型にキャストしてから、toDate()メソッドでDateTime型に変換する

だけです。

前置きが長かったですね、ごめんなさい…笑

一応「?.」演算子で値がnullの時に回避できるようにしてます。

まとめ

  • Firestoreから取得した日付データはTimestamp型
  • DateTime型へ変換したいときは、「一旦Timestamp型にキャストした後、DateTime型へ変換する」
  • 保存時はそのままでOK