FlutterのModelをfreezedで自動生成する（firebaseのDateTimeもあり）


# はじめに
[freezed]()というパッケージが良い。immutableが良い！っていうのを
聞いてはいたものの、使い方がいまいち分かっていなかった状態でした。

また、cloud_firestoreのモデルを生成したかったのですが、DateTimeで引っかかってしまったので、備忘録兼ねて記事を残します。

## パッケージ
まずは、以下のパッケージを入れて`pub get`しましょう

```pubspec.yaml
dependencies:
 cloud_firestore: ^3.1.15 # firestoreのDateTimeを扱うため
 freezed_annotation:
 json_serializable: # toJson/fromJsonを生成するため

dev_dependencies:
 build_runner:
 freezed:
```

## クラスの定義
今回は、無難にUserクラスを自動生成していきましょう。
まず、inport文にこのように記述してください。
```user.dart
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

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

次に、DateTime用のクラスを作成します。
[こちらの記事](https://qiita.com/GleamingCake/items/4ebdc27a4dc3cbd07458)を参考にさせていただきました。
一番下に、コピペしてください。

```user.dart
class CreatedAtField implements JsonConverter<DateTime?, dynamic> {
  const CreatedAtField();

  @override
  DateTime? fromJson(dynamic timestamp) {
    timestamp as Timestamp?;
    return timestamp?.toDate();
  }

  @override
  dynamic toJson(DateTime? dateTime) {
    if (dateTime == null) return FieldValue.serverTimestamp();
    return dateTime;
  }
}

class UpdatedAtField implements JsonConverter<DateTime?, dynamic> {
  const UpdatedAtField();

  @override
  DateTime? fromJson(dynamic timestamp) {
    timestamp as Timestamp?;
    return timestamp?.toDate();
  }

  @override
  FieldValue toJson(DateTime? date) {
    return FieldValue.serverTimestamp();
  }
}

```

次に、メイン部分を記述します。
```user.dart
@freezed
abstract class User with _$User {
  const factory User({
    @JsonKey(name: 'userId') required String userId,
    @JsonKey(name: 'userName') required String title,
    @JsonKey(name: 'age') required int userLevel,
    @JsonKey(name: 'gender') required List<String> userList,
    @JsonKey(name: 'createdAt') @CreatedAtField() DateTime? createdAt,
    @JsonKey(name: 'updatedAt') @UpdatedAtField() DateTime? updatedAt,
  }) = _User;

  const User._();

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

`@freezed`を書くことでコマンドを実行した時の自動生成の対象となります。

では、コードの全体の確認と、コマンドの実行をしてみましょう。
コマンドを実行するまでは、`user.dart`はエラーが出ている状態だと思いますが、正常に実行できればエラーは解消されます。

## 全体
```user.dart
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

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

@freezed
abstract class User with _$User {
  const factory User({
    @JsonKey(name: 'userId') required String userId,
    @JsonKey(name: 'userName') required String title,
    @JsonKey(name: 'age') required int userLevel,
    @JsonKey(name: 'gender') required List<String> userList,
    @JsonKey(name: 'createdAt') @CreatedAtField() DateTime? createdAt,
    @JsonKey(name: 'updatedAt') @UpdatedAtField() DateTime? updatedAt,
  }) = _User;

  const User._();

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

// プロパティにつけると変換時、そのプロパティが null なら FieldValue.serverTimestamp() に変換する
// これにより、クライアント側で作成したときのみサーバー側で時間が書き込まれるため、createdAt の挙動になる
class CreatedAtField implements JsonConverter<DateTime?, dynamic> {
  const CreatedAtField();

  @override
  DateTime? fromJson(dynamic timestamp) {
    timestamp as Timestamp?;
    return timestamp?.toDate();
  }

  // nullの時は toJson 時 FieldValue.serverTimestamp() を返すことで、createdAt の挙動になる
  @override
  dynamic toJson(DateTime? dateTime) {
    if (dateTime == null) return FieldValue.serverTimestamp();
    return dateTime;
  }
}

// プロパティにつけると変換時、必ず FieldValue.serverTimestamp() に変換される
// これにより、updatedAt の挙動になる
class UpdatedAtField implements JsonConverter<DateTime?, dynamic> {
  const UpdatedAtField();

  @override
  DateTime? fromJson(dynamic timestamp) {
    timestamp as Timestamp?;
    return timestamp?.toDate();
  }

  @override
  FieldValue toJson(DateTime? date) {
    return FieldValue.serverTimestamp();
  }
}
```
## 実行コマンド
`--delete-conflicting-outputs`のオプションはあった方が良さそうです。
```
$  flutter pub run build_runner build --delete-conflicting-outputs
```

エラーなく完了したら、`user.dart`と同じディレクトリに`user.g.dart`と`user.freezed.dart`が生成されていると思います。

```user.g.dart
// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'user.dart';

// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************

_$_User _$$_UserFromJson(Map<String, dynamic> json) => _$_User(
      userId: json['userId'] as String,
      title: json['userName'] as String,
      userLevel: json['age'] as int,
      userList:
          (json['gender'] as List<dynamic>).map((e) => e as String).toList(),
      createdAt: const CreatedAtField().fromJson(json['createdAt']),
      updatedAt: const UpdatedAtField().fromJson(json['updatedAt']),
    );

Map<String, dynamic> _$$_UserToJson(_$_User instance) => <String, dynamic>{
      'userId': instance.userId,
      'userName': instance.title,
      'age': instance.userLevel,
      'gender': instance.userList,
      'createdAt': const CreatedAtField().toJson(instance.createdAt),
      'updatedAt': const UpdatedAtField().toJson(instance.updatedAt),
    };

```

```user.freezed.dart
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target

part of 'user.dart';

// **************************************************************************
// FreezedGenerator
// **************************************************************************

T _$identity<T>(T value) => value;

final _privateConstructorUsedError = UnsupportedError(
    'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');

User _$UserFromJson(Map<String, dynamic> json) {
  return _User.fromJson(json);
}

/// @nodoc
mixin _$User {
  @JsonKey(name: 'userId')
  String get userId => throw _privateConstructorUsedError;
  @JsonKey(name: 'userName')
  String get title => throw _privateConstructorUsedError;
  @JsonKey(name: 'age')
  int get userLevel => throw _privateConstructorUsedError;
  @JsonKey(name: 'gender')
  List<String> get userList => throw _privateConstructorUsedError;
  @JsonKey(name: 'createdAt')
  @CreatedAtField()
  DateTime? get createdAt => throw _privateConstructorUsedError;
  @JsonKey(name: 'updatedAt')
  @UpdatedAtField()
  DateTime? get updatedAt => throw _privateConstructorUsedError;

  Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
  @JsonKey(ignore: true)
  $UserCopyWith<User> get copyWith => throw _privateConstructorUsedError;
}

/// @nodoc
abstract class $UserCopyWith<$Res> {
  factory $UserCopyWith(User value, $Res Function(User) then) =
      _$UserCopyWithImpl<$Res>;
  $Res call(
      {@JsonKey(name: 'userId') String userId,
      @JsonKey(name: 'userName') String title,
      @JsonKey(name: 'age') int userLevel,
      @JsonKey(name: 'gender') List<String> userList,
      @JsonKey(name: 'createdAt') @CreatedAtField() DateTime? createdAt,
      @JsonKey(name: 'updatedAt') @UpdatedAtField() DateTime? updatedAt});
}

/// @nodoc
class _$UserCopyWithImpl<$Res> implements $UserCopyWith<$Res> {
  _$UserCopyWithImpl(this._value, this._then);

  final User _value;
  // ignore: unused_field
  final $Res Function(User) _then;

  @override
  $Res call({
    Object? userId = freezed,
    Object? title = freezed,
    Object? userLevel = freezed,
    Object? userList = freezed,
    Object? createdAt = freezed,
    Object? updatedAt = freezed,
  }) {
    return _then(_value.copyWith(
      userId: userId == freezed
          ? _value.userId
          : userId // ignore: cast_nullable_to_non_nullable
              as String,
      title: title == freezed
          ? _value.title
          : title // ignore: cast_nullable_to_non_nullable
              as String,
      userLevel: userLevel == freezed
          ? _value.userLevel
          : userLevel // ignore: cast_nullable_to_non_nullable
              as int,
      userList: userList == freezed
          ? _value.userList
          : userList // ignore: cast_nullable_to_non_nullable
              as List<String>,
      createdAt: createdAt == freezed
          ? _value.createdAt
          : createdAt // ignore: cast_nullable_to_non_nullable
              as DateTime?,
      updatedAt: updatedAt == freezed
          ? _value.updatedAt
          : updatedAt // ignore: cast_nullable_to_non_nullable
              as DateTime?,
    ));
  }
}

/// @nodoc
abstract class _$$_UserCopyWith<$Res> implements $UserCopyWith<$Res> {
  factory _$$_UserCopyWith(_$_User value, $Res Function(_$_User) then) =
      __$$_UserCopyWithImpl<$Res>;
  @override
  $Res call(
      {@JsonKey(name: 'userId') String userId,
      @JsonKey(name: 'userName') String title,
      @JsonKey(name: 'age') int userLevel,
      @JsonKey(name: 'gender') List<String> userList,
      @JsonKey(name: 'createdAt') @CreatedAtField() DateTime? createdAt,
      @JsonKey(name: 'updatedAt') @UpdatedAtField() DateTime? updatedAt});
}

/// @nodoc
class __$$_UserCopyWithImpl<$Res> extends _$UserCopyWithImpl<$Res>
    implements _$$_UserCopyWith<$Res> {
  __$$_UserCopyWithImpl(_$_User _value, $Res Function(_$_User) _then)
      : super(_value, (v) => _then(v as _$_User));

  @override
  _$_User get _value => super._value as _$_User;

  @override
  $Res call({
    Object? userId = freezed,
    Object? title = freezed,
    Object? userLevel = freezed,
    Object? userList = freezed,
    Object? createdAt = freezed,
    Object? updatedAt = freezed,
  }) {
    return _then(_$_User(
      userId: userId == freezed
          ? _value.userId
          : userId // ignore: cast_nullable_to_non_nullable
              as String,
      title: title == freezed
          ? _value.title
          : title // ignore: cast_nullable_to_non_nullable
              as String,
      userLevel: userLevel == freezed
          ? _value.userLevel
          : userLevel // ignore: cast_nullable_to_non_nullable
              as int,
      userList: userList == freezed
          ? _value._userList
          : userList // ignore: cast_nullable_to_non_nullable
              as List<String>,
      createdAt: createdAt == freezed
          ? _value.createdAt
          : createdAt // ignore: cast_nullable_to_non_nullable
              as DateTime?,
      updatedAt: updatedAt == freezed
          ? _value.updatedAt
          : updatedAt // ignore: cast_nullable_to_non_nullable
              as DateTime?,
    ));
  }
}

/// @nodoc
@JsonSerializable()
class _$_User extends _User with DiagnosticableTreeMixin {
  const _$_User(
      {@JsonKey(name: 'userId') required this.userId,
      @JsonKey(name: 'userName') required this.title,
      @JsonKey(name: 'age') required this.userLevel,
      @JsonKey(name: 'gender') required final List<String> userList,
      @JsonKey(name: 'createdAt') @CreatedAtField() this.createdAt,
      @JsonKey(name: 'updatedAt') @UpdatedAtField() this.updatedAt})
      : _userList = userList,
        super._();

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

  @override
  @JsonKey(name: 'userId')
  final String userId;
  @override
  @JsonKey(name: 'userName')
  final String title;
  @override
  @JsonKey(name: 'age')
  final int userLevel;
  final List<String> _userList;
  @override
  @JsonKey(name: 'gender')
  List<String> get userList {
    // ignore: implicit_dynamic_type
    return EqualUnmodifiableListView(_userList);
  }

  @override
  @JsonKey(name: 'createdAt')
  @CreatedAtField()
  final DateTime? createdAt;
  @override
  @JsonKey(name: 'updatedAt')
  @UpdatedAtField()
  final DateTime? updatedAt;

  @override
  String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
    return 'User(userId: $userId, title: $title, userLevel: $userLevel, userList: $userList, createdAt: $createdAt, updatedAt: $updatedAt)';
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties
      ..add(DiagnosticsProperty('type', 'User'))
      ..add(DiagnosticsProperty('userId', userId))
      ..add(DiagnosticsProperty('title', title))
      ..add(DiagnosticsProperty('userLevel', userLevel))
      ..add(DiagnosticsProperty('userList', userList))
      ..add(DiagnosticsProperty('createdAt', createdAt))
      ..add(DiagnosticsProperty('updatedAt', updatedAt));
  }

  @override
  bool operator ==(dynamic other) {
    return identical(this, other) ||
        (other.runtimeType == runtimeType &&
            other is _$_User &&
            const DeepCollectionEquality().equals(other.userId, userId) &&
            const DeepCollectionEquality().equals(other.title, title) &&
            const DeepCollectionEquality().equals(other.userLevel, userLevel) &&
            const DeepCollectionEquality().equals(other._userList, _userList) &&
            const DeepCollectionEquality().equals(other.createdAt, createdAt) &&
            const DeepCollectionEquality().equals(other.updatedAt, updatedAt));
  }

  @JsonKey(ignore: true)
  @override
  int get hashCode => Object.hash(
      runtimeType,
      const DeepCollectionEquality().hash(userId),
      const DeepCollectionEquality().hash(title),
      const DeepCollectionEquality().hash(userLevel),
      const DeepCollectionEquality().hash(_userList),
      const DeepCollectionEquality().hash(createdAt),
      const DeepCollectionEquality().hash(updatedAt));

  @JsonKey(ignore: true)
  @override
  _$$_UserCopyWith<_$_User> get copyWith =>
      __$$_UserCopyWithImpl<_$_User>(this, _$identity);

  @override
  Map<String, dynamic> toJson() {
    return _$$_UserToJson(this);
  }
}

abstract class _User extends User {
  const factory _User(
      {@JsonKey(name: 'userId')
          required final String userId,
      @JsonKey(name: 'userName')
          required final String title,
      @JsonKey(name: 'age')
          required final int userLevel,
      @JsonKey(name: 'gender')
          required final List<String> userList,
      @JsonKey(name: 'createdAt')
      @CreatedAtField()
          final DateTime? createdAt,
      @JsonKey(name: 'updatedAt')
      @UpdatedAtField()
          final DateTime? updatedAt}) = _$_User;
  const _User._() : super._();

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

  @override
  @JsonKey(name: 'userId')
  String get userId => throw _privateConstructorUsedError;
  @override
  @JsonKey(name: 'userName')
  String get title => throw _privateConstructorUsedError;
  @override
  @JsonKey(name: 'age')
  int get userLevel => throw _privateConstructorUsedError;
  @override
  @JsonKey(name: 'gender')
  List<String> get userList => throw _privateConstructorUsedError;
  @override
  @JsonKey(name: 'createdAt')
  @CreatedAtField()
  DateTime? get createdAt => throw _privateConstructorUsedError;
  @override
  @JsonKey(name: 'updatedAt')
  @UpdatedAtField()
  DateTime? get updatedAt => throw _privateConstructorUsedError;
  @override
  @JsonKey(ignore: true)
  _$$_UserCopyWith<_$_User> get copyWith => throw _privateConstructorUsedError;
}
```

こうしてすんなり生成できることを思うと、なんとなく良さが分かってきました。
次の記事で、このfreezedで生成したクラスを使用する方法を記載します。

ありがとうございました。