Dartはすごいけど、「モデル」を定義するのは面倒くさい。しなければならないかもしれません。
コンストラクタを定義する + プロパティを定義する
オーバーライド toString, operator ==, hashCode
オブジェクトを複製するためのcopyWithメソッドの実装
de/シリアライズの処理
その上、Dartにはユニオン型やパターンマッチングなどの機能も欠けています。
これらをすべて実装すると、何百行にもなり、エラーが発生しやすく、モデルの可読性に大きく影響します。
Freezed はこれらの問題を解決しようとします。
翻訳するとこのように書かれていました
自分でモデルクラスを作ると、以下のように自分で設定ファイルを考えないといけないのですが、このパッケージはその問題を解決してくれます。
しかし、作るモデルクラスによっては、自作しないと作れないものもあるそうです?
// アプリのデータを保存するモデル
import 'package:cloud_firestore/cloud_firestore.dart';
class Diary {
String? docId;
Timestamp date;
String title;
String review;
String image;
Diary(this.date, this.title, this.review, this.image);
factory Diary.toModel(String docId, Map<String, dynamic> json) {
final diary = Diary(
json['date'],
json['title'],
json['review'],
json['image'],
);
diary.docId = docId;
return diary;
}
Map<String, dynamic> toJson() {
return {
'date': date,
'title': title,
'review': review,
'image': image,
};
}
}
さくしんさんの記事を参考にすると環境構築が速くできます!
必要なパッケージをインストールする
flutter pub add freezed_annotation
flutter pub add build_runner --dev
flutter pub add freezed --dev
flutter pub add json_serializable --dev
flutter pub add json_annotation
さくしんさんのテンプレートを使うと、Freezedでモデルクラスを簡単に作ることができます。ですが、FirebaseのTimestampを使用する場合は、コンバーターと呼ばれるものを自作する必要があります。
こちらの記事が参考になります
こちら必要になります。
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
class TimestampConverter implements JsonConverter<DateTime?, Timestamp?> {
const TimestampConverter();
@override
DateTime? fromJson(Timestamp? json) => json?.toDate();
@override
Timestamp? toJson(DateTime? object) =>
object == null ? null : Timestamp.fromDate(object);
}
Freezedのコマンドを実行したときに以下のエラーが発生したときは、私の記事を参考に対応すれば解決できると思われます。
pub finished with exit code 66
ターミナルで以下のコマンドを実行
① flutter pub pub cache repair と打つ。
flutter pub pub cache repair
② flutter packages get と打つ。
flutter packages get
③ flutter pub run build_runner watch --delete-conflicting-outputsと打つ。
watchモードになってずっと起動しているので、止めるときはMacだと control + c を押す!
flutter pub run build_runner watch --delete-conflicting-outputs
日記アプリのモデルクラスを定義してみました。過去の記事を見ると、Datetime型でもコンバーターがいることが、書いてあるますが、今のところエラーが出てこないので、新しいバージョンは対応できているのかもしれないですね。
Freezedの良いところは、toJSON,fromJSON,copyWithを自分でコードを書かなくても作ってくれます。
これを使うことで、Firebase,APIを使うモデルクラスを作るのが、楽になります。
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';
part 'note_model.freezed.dart';
part 'note_model.g.dart';
class NoteModel with _$NoteModel {
const factory NoteModel({
required String id,
required String body,
required DateTime createdAt,
}) = _NoteModel;
factory NoteModel.fromJson(Map<String, dynamic> json) =>
_$NoteModelFromJson(json);
}
自動生成されたファイル
// 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, unnecessary_question_mark
part of 'note_model.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');
NoteModel _$NoteModelFromJson(Map<String, dynamic> json) {
return _NoteModel.fromJson(json);
}
/// @nodoc
mixin _$NoteModel {
String get id => throw _privateConstructorUsedError;
String get body => throw _privateConstructorUsedError;
DateTime get createdAt => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
(ignore: true)
$NoteModelCopyWith<NoteModel> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $NoteModelCopyWith<$Res> {
factory $NoteModelCopyWith(NoteModel value, $Res Function(NoteModel) then) =
_$NoteModelCopyWithImpl<$Res, NoteModel>;
$Res call({String id, String body, DateTime createdAt});
}
/// @nodoc
class _$NoteModelCopyWithImpl<$Res, $Val extends NoteModel>
implements $NoteModelCopyWith<$Res> {
_$NoteModelCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
('vm:prefer-inline')
$Res call({
Object? id = null,
Object? body = null,
Object? createdAt = null,
}) {
return _then(_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
body: null == body
? _value.body
: body // ignore: cast_nullable_to_non_nullable
as String,
createdAt: null == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,
) as $Val);
}
}
/// @nodoc
abstract class _$$_NoteModelCopyWith<$Res> implements $NoteModelCopyWith<$Res> {
factory _$$_NoteModelCopyWith(
_$_NoteModel value, $Res Function(_$_NoteModel) then) =
__$$_NoteModelCopyWithImpl<$Res>;
$Res call({String id, String body, DateTime createdAt});
}
/// @nodoc
class __$$_NoteModelCopyWithImpl<$Res>
extends _$NoteModelCopyWithImpl<$Res, _$_NoteModel>
implements _$$_NoteModelCopyWith<$Res> {
__$$_NoteModelCopyWithImpl(
_$_NoteModel _value, $Res Function(_$_NoteModel) _then)
: super(_value, _then);
('vm:prefer-inline')
$Res call({
Object? id = null,
Object? body = null,
Object? createdAt = null,
}) {
return _then(_$_NoteModel(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
body: null == body
? _value.body
: body // ignore: cast_nullable_to_non_nullable
as String,
createdAt: null == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,
));
}
}
/// @nodoc
()
class _$_NoteModel with DiagnosticableTreeMixin implements _NoteModel {
const _$_NoteModel(
{required this.id, required this.body, required this.createdAt});
factory _$_NoteModel.fromJson(Map<String, dynamic> json) =>
_$$_NoteModelFromJson(json);
final String id;
final String body;
final DateTime createdAt;
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
return 'NoteModel(id: $id, body: $body, createdAt: $createdAt)';
}
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty('type', 'NoteModel'))
..add(DiagnosticsProperty('id', id))
..add(DiagnosticsProperty('body', body))
..add(DiagnosticsProperty('createdAt', createdAt));
}
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$_NoteModel &&
(identical(other.id, id) || other.id == id) &&
(identical(other.body, body) || other.body == body) &&
(identical(other.createdAt, createdAt) ||
other.createdAt == createdAt));
}
(ignore: true)
int get hashCode => Object.hash(runtimeType, id, body, createdAt);
(ignore: true)
('vm:prefer-inline')
_$$_NoteModelCopyWith<_$_NoteModel> get copyWith =>
__$$_NoteModelCopyWithImpl<_$_NoteModel>(this, _$identity);
Map<String, dynamic> toJson() {
return _$$_NoteModelToJson(
this,
);
}
}
abstract class _NoteModel implements NoteModel {
const factory _NoteModel(
{required final String id,
required final String body,
required final DateTime createdAt}) = _$_NoteModel;
factory _NoteModel.fromJson(Map<String, dynamic> json) =
_$_NoteModel.fromJson;
String get id;
String get body;
DateTime get createdAt;
(ignore: true)
_$$_NoteModelCopyWith<_$_NoteModel> get copyWith =>
throw _privateConstructorUsedError;
}
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'note_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$_NoteModel _$$_NoteModelFromJson(Map<String, dynamic> json) => _$_NoteModel(
id: json['id'] as String,
body: json['body'] as String,
createdAt: DateTime.parse(json['createdAt'] as String),
);
Map<String, dynamic> _$$_NoteModelToJson(_$_NoteModel instance) =>
<String, dynamic>{
'id': instance.id,
'body': instance.body,
'createdAt': instance.createdAt.toIso8601String(),
};
モデルクラスを使用して、画面にダミーのデータですが表示して詳細ページにデータを渡して表示するサンプルを作ってみました。
Firebaseを使用した例も過去に記事にしたことがあるので、ご興味あれば見てみてください。
記事にしてはいないのですが、Dioを使用して、JSONPlaceholderからデータをHTTP通信のGETをして表示するサンプルも作成しました。
データを画面に表示するページ
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/material.dart';
import 'package:flutter_template/about_freezed/note_model/note_model.dart';
import 'package:flutter_template/about_freezed/ui/detail_page.dart';
// Providerにfamilyを使ってuserIDをパラメーターとして渡す
final noteProvider = Provider.family<NoteModel, String>((ref, userID) {
if (userID == '0X11') {
return NoteModel(id: '0x11', body: '今日は天気が良い', createdAt: DateTime.now());
}
if (userID == '0x22') {
return NoteModel(id: '0x22', body: '今日は天気が良い', createdAt: DateTime.now());
}
return NoteModel(id: '0000', body: 'null', createdAt: DateTime.now());
});
final noteIDsProvider = Provider<List<String>>((ref) {
return ['0X11', '0x22'];
});
final notesProvider = Provider((ref) {
final userIDs = ref.watch(noteIDsProvider);
return [
for (final userID in userIDs) ref.watch(noteProvider(userID)),
];
});
class NotePage extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final notes = ref.watch(notesProvider);
return Scaffold(
appBar: AppBar(
title: const Text('NoteFamily'),
),
body: Column(
children: [
// Widgetだとfor文はreturnがない
for (final note in notes)
ListTile(
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => DetailScreen(note: note)));
},
title: Text(note.id),
subtitle: Row(
children: [
Text(note.body),
const SizedBox(width: 20),
Text(note.createdAt.toIso8601String())
],
),
),
],
),
);
}
}
データを受け取って表示する画面遷移先のページ
import 'package:flutter/material.dart';
import 'package:flutter_template/about_freezed/note_model/note_model.dart';
class DetailScreen extends StatelessWidget {
// コンストラクタで、Todoを要求します。
const DetailScreen({super.key, required this.note});
// Todoを保持するフィールドを宣言する。
final NoteModel note;
Widget build(BuildContext context) {
// Todoを使用してUIを作成します。
return Scaffold(
appBar: AppBar(
title: Text(note.id),
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(note.body),
),
),
);
}
}