【Flutter】【Dart】コードジェネレータFreezedでイミュータブル/ミュータブルなモデルを生成する

Dartでイミュータブル/ミュータブルなモデルを作るFreezedの基本的な使い方をまとめました。

Flutter 3.19.4
Freezed 2.4.1

Freezedとは?

いわゆるモデルクラスを作る時には、プロパティを書いて、コンストラクタを書いて、場合によってはディープコピー用のコードやJsonシリアライズする処理を書いて・・と結構な量のボイラープレートを書く必要があります。

Freezedはコードの一部だけを書けば、上記のようなボイラープレートの大半を生成してくれるコードジェネレータです。

pub.dev

本記事ではこれの基本的な使い方についてまとめます。

セットアップ

インストールするには以下のコマンドを叩きます。

flutter pub add freezed_annotation
flutter pub add dev:build_runner
flutter pub add dev:freezed

Jsonへのシリアライズ、デシリアライズコードを生成する機能も使いたいのであれば、以下のパッケージも一緒にインストールします。

flutter pub add json_annotation
flutter pub add dev:json_serializable

さらに、json_annotation を使う場合、analysis_options.yamlに以下を追記します。

analyzer:
  errors:
    invalid_annotation_target: ignore

ここまででインストールは終わりです。

イミュータブルなモデルを作る

それではまずイミュータブルなクラスを作ってみます。
example.dart を作って以下のように記述します。

import 'package:freezed_annotation/freezed_annotation.dart';

// Freezedはpartとしてコードを生成するので、このように書いておく
part 'example.freezed.dart';

// Jsonによるシリアライズ・デシリアライズをする場合にはこちらも
part 'example.g.dart';

// ミュータブルなクラスを生成する場合はfreezedアノテーションを付ける
@freezed
class Person with _$Person {
  const factory Person({
    required String firstName,
    required String lastName,
    required int age,
  }) = _Person;

  // Jsonによるシリアライズ・デシリアライズをする場合には必要
  // fromJsonだけこのように書いておくと、Freezedが自動でtoJsonも生成してくれる
  factory Person.fromJson(Map<String, Object?> json) => _$PersonFromJson(json);
}

説明はコメントに書いた通りです。ほぼボイラープレートです。
なおこの時点ではエラーが出ますが、コード生成後に消えるので気にしないでください。

次に以下のコマンドを叩いてコードを生成します。

dart run build_runner build

example.freeezed.dartexample.g.dart が生成され、エラーが消えたことを確認できます。

生成された

以下のように、イミュータブルなモデルが生成されることを確認できました。

import 'example.dart';

void main() {
  final person = Person(
    firstName: 'John',
    lastName: 'Doe',
    age: 30,
  );

  // イミュータブルなので↓はエラー
  //person.firstName = 'Jane';
  
  final personJson = person.toJson();
  final personFromJson = Person.fromJson(personJson);

  print(person);
  print(personJson);
  print(personFromJson);
}

ちなみに都度ビルドするのが面倒な時には以下のコマンドを走らせておくと勝手にビルドしてくれます。

dart run build_runner watch

ミュータブルなモデルを作る

次にミュータブルなクラスを作ります。
ミュータブルにするには、前節でつけていたfreezedアノテーションunfreezedアノテーションに変えます。
その他にもいくつか変更点があるので、以下のコメントに記述します。

import 'package:freezed_annotation/freezed_annotation.dart';

part 'example.freezed.dart';

part 'example.g.dart';

// unfreezedアノテーションに
@unfreezed
class Person with _$Person {
  // const factoryではなくfactoryに
  factory Person({
    required String firstName,
    required String lastName,
    required final int age, // イミュータブルなメンバしたい場合はfinalに
  }) = _Person;

  factory Person.fromJson(Map<String, Object?> json) => _$PersonFromJson(json);
}

これでコード生成すると、以下のようにミュータブルなクラスができていることを確認できます。

import 'example.dart';

void main() {
  final person = Person(
    firstName: 'John',
    lastName: 'Doe',
    age: 30,
  );

  // ミュータブルなので↓はエラーにならない
  person.firstName = 'Jane';
  
  // ageはイミュータブルなので↓はエラーになる
  //person.age = 31;

  final personJson = person.toJson();
  final personFromJson = Person.fromJson(personJson);

  print(person);
  print(personJson);
  print(personFromJson);
}

List、Map、Setの挙動

freezedをつけた場合、ListMapSetの要素の操作ついてもイミュータブルになります。
つまり以下のように、変更を加えようとするとランタイムエラーになります。

import 'package:freezed_annotation/freezed_annotation.dart';

part 'main.freezed.dart';

@freezed
class Example with _$Example {
  factory Example(
    List<int> list,
  ) = _Example;
}

void main() {
  var example = Example([]);
  // ランタイムエラーになる
  example.list.add(123);
}

要素の操作だけは許可したい、という場合は、以下のように書きます。

import 'package:freezed_annotation/freezed_annotation.dart';

part 'main.freezed.dart';

@Freezed(makeCollectionsUnmodifiable: false)
class Example with _$Example {
  factory Example(
    List<int> list,
  ) = _Example;
}

void main() {
  var example = Example([]);
  // ランタイムエラーにならない(ビルドはしなおしてください)
  example.list.add(123);
}

モデルをコピーする

Freezed で作ったモデルにはそのモデルをコピーするメソッド copyWith もつくられるので、以下のようにモデルを複製することができます。

import 'package:freezed_annotation/freezed_annotation.dart';

part 'main.freezed.dart';

@freezed
class Example with _$Example {
  const factory Example(
    int a,
    int b,
  ) = _Example;
}

void main() {
  var example = Example(1, 2);
  
  // コピー
  var copy = example.copyWith(a: 3);

  // example: Example(1, 2) -> copy: Example(3, 2)
  print('example: $example -> copy: $copy');
}

クラスに参照関係があってもディープコピーされます。
もし参照しているクラスの一部のメンバだけ変更しつつコピーしたい場合は以下のようにします。

import 'package:freezed_annotation/freezed_annotation.dart';

part 'main.freezed.dart';

@freezed
class Example with _$Example {
  const factory Example(
    int a,
    Child b,
  ) = _Example;
}

@freezed
class Child with _$Child {
  const factory Child(
    int c,
    int d,
  ) = _Child;
}

void main() {
  var example = Example(1, Child(2, 3));
  // Child.cだけ変更したい場合はこのように書く
  var copy = example.copyWith.b(c: 123);

  // example: Example(a: 1, b: Child(c: 2, d: 3)) -> copy: Example(a: 1, b: Child(c: 123, d: 3))
  print('example: $example -> copy: $copy');
}

メンバを自分で定義したい場合

メソッドなどのメンバを自分で定義したい場合には、以下のようにprivateな引数なしコンストラクトを書く必要があります。(書かないとコンパイルエラーになります)

import 'package:freezed_annotation/freezed_annotation.dart';

part 'main.freezed.dart';

@freezed
class Example with _$Example {
  // メンバを自分で定義する場合引数なしのprivateコンストラクタを定義する必要がある
  const Example._();

  const factory Example(
    int a,
    int b,
  ) = _Example;

  // メンバを自分で定義
  void test() {
    print('test');
  }
}

void main() {
  var example = Example(1, 2);
  example.test();
}

CopyWithや==オペレータを生成しない

以下のように書くと、CopyWithや==オペレータを生成しないようにできます。

import 'package:freezed_annotation/freezed_annotation.dart';

part 'main.freezed.dart';

// CopyWithと==オペレータを生成しない
@Freezed(copyWith: false, equal: false)
class Example with _$Example {
  factory Example(int a) = _Example;
}

プロジェクト全体としての設定を変更するには、pubspec.yaml と同じ階層に build.yaml を作成し、以下のように記述します。

targets:
  $default:
    builders:
      freezed:
        options:
          # CopyWithを生成しない
          copy_with: false
          # == オペレータを生成しない
          equal: false

IntelliJプラグイン

Freezed用のIntelliJ用のライブテンプレートがFlutter Freezed Snippetsという名前のプラグインとして提供されています。

Flutter Freezed Snippets

これを使うと例えば、 freezedClass と打つだけ(実際には全部打つ必要ないけど)で以下のようなコードが簡単に生成できたり、

@freezed
class Test with _$Test {
}

freezedFromJson と打つといかが生成できたりします。

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

他にもあるので、とりあえず入れてfreezedと打ってみるといいと思います。便利です。

なお他のエディタ用のもあるようです。

参考

その他本記事にまとめきれていない情報などは公式のドキュメントを参照してください。

pub.dev