はじめに
freezedというパッケージが良い。immutableが良い!っていうのを
聞いてはいたものの、使い方がいまいち分かっていなかった状態でした。
また、cloud_firestoreのモデルを生成したかったのですが、DateTimeで引っかかってしまったので、備忘録兼ねて記事を残します。
パッケージ
まずは、以下のパッケージを入れてpub get
しましょう
dependencies:
cloud_firestore: ^3.1.15 # firestoreのDateTimeを扱うため
freezed_annotation:
json_serializable: # toJson/fromJsonを生成するため
dev_dependencies:
build_runner:
freezed:
クラスの定義
今回は、無難にUserクラスを自動生成していきましょう。
まず、inport文にこのように記述してください。
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用のクラスを作成します。
こちらの記事を参考にさせていただきました。
一番下に、コピペしてください。
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();
}
}
次に、メイン部分を記述します。
@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
はエラーが出ている状態だと思いますが、正常に実行できればエラーは解消されます。
全体
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
が生成されていると思います。
// 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),
};
// 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で生成したクラスを使用する方法を記載します。
ありがとうございました。