Chapter 12

Freezedを使ってみる

JboyHashimoto
JboyHashimoto
2023.02.27に更新

https://pub.dev/packages/freezed
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,
    };
  }
}

さくしんさんの記事を参考にすると環境構築が速くできます!
https://zenn.dev/sakusin/articles/b19e9a2c3829e0

必要なパッケージをインストールする

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を使用する場合は、コンバーターと呼ばれるものを自作する必要があります。

こちらの記事が参考になります
https://note.com/saburo_engineer/n/nac701776e0ea

こちら必要になります。

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

https://zenn.dev/flutteruniv_dev/articles/2f7ebb605ee490

ターミナルで以下のコマンドを実行

① 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を使うモデルクラスを作るのが、楽になります。

note_model.dart
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);
}

自動生成されたファイル

note_model.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, 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;
}
note_model.g.dart
// 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を使用した例も過去に記事にしたことがあるので、ご興味あれば見てみてください。
https://zenn.dev/flutteruniv_dev/articles/0edaf38f13169f
記事にしてはいないのですが、Dioを使用して、JSONPlaceholderからデータをHTTP通信のGETをして表示するサンプルも作成しました。
https://github.com/sakurakotubaki/DioTutorial

データを画面に表示するページ

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),
        ),
      ),
    );
  }
}