11
2

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 1 year has passed since last update.

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

Posted at

はじめに

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用のクラスを作成します。
こちらの記事を参考にさせていただきました。
一番下に、コピペしてください。

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.dartuser.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で生成したクラスを使用する方法を記載します。

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

11
2
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
11
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?