Dartでイミュータブル/ミュータブルなモデルを作るFreezedの基本的な使い方をまとめました。
- Freezedとは?
- セットアップ
- イミュータブルなモデルを作る
- ミュータブルなモデルを作る
- List、Map、Setの挙動
- モデルをコピーする
- メンバを自分で定義したい場合
- CopyWithや==オペレータを生成しない
- IntelliJ用プラグイン
- 参考
Flutter 3.19.4
Freezed 2.4.1
Freezedとは?
いわゆるモデルクラスを作る時には、プロパティを書いて、コンストラクタを書いて、場合によってはディープコピー用のコードやJsonにシリアライズする処理を書いて・・と結構な量のボイラープレートを書く必要があります。
Freezedはコードの一部だけを書けば、上記のようなボイラープレートの大半を生成してくれるコードジェネレータです。
本記事ではこれの基本的な使い方についてまとめます。
セットアップ
インストールするには以下のコマンドを叩きます。
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.dart と example.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
をつけた場合、List
やMap
、Set
の要素の操作ついてもイミュータブルになります。
つまり以下のように、変更を加えようとするとランタイムエラーになります。
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という名前のプラグインとして提供されています。
これを使うと例えば、 freezedClass と打つだけ(実際には全部打つ必要ないけど)で以下のようなコードが簡単に生成できたり、
@freezed class Test with _$Test { }
freezedFromJson と打つといかが生成できたりします。
factory Test.fromJson(Map<String, dynamic> json) => _$TestFromJson(json);
他にもあるので、とりあえず入れてfreezedと打ってみるといいと思います。便利です。
なお他のエディタ用のもあるようです。
参考
その他本記事にまとめきれていない情報などは公式のドキュメントを参照してください。