Contents
How to use
Install
Freezedを使うには、通常のbuild_runner / code-generatorセットアップが必要です。
まず、pubspec.yamlファイルにbuild_runnerとFreezedを追加してインストールします。
# pubspec.yaml
dependencies:
freezed_annotation:
dev_dependencies:
build_runner:
freezed:
これ↑により3つのパッケージをインストールします。
- build_runner,コード生成機能を提供します。
- freezed,コード生成機能です。
- freezed_annotation, freezedのためのアノテーション機能を提供します。
Run the generator
ほとんどのコード生成パッケージと同様に、Freezedを使うためにはannotation(meta)のインポートと、ファイルの先頭でpartキーワードを使う必要があります。
Freezedを使うファイルの先頭で、このようにコードを記述してください。
import 'package:freezed_annotation/freezed_annotation.dart';
part 'my_file.freezed.dart';
package:flutter/foundation.dartのインポートも検討してください。
その理由は、foundation.dartのインポートにより、Flutter devtool内で可読性の高いオブジェクトを生成するためのクラスもインポートされるからです。
foundation.dartをインポートすると、Freezedは自動的に対処します。
以下のコードを参考にしてください。
// main.dart
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';
part 'main.freezed.dart';
@freezed
abstract class Union with _$Union {
const factory Union(int value) = Data;
const factory Union.loading() = Loading;
const factory Union.error([String message]) = ErrorDetails;
}
そこから、コードジェネレーターを実行するには、次の2つの可能性があります。
●パッケージがFlutterに依存している場合は
flutter pub run build_runner build
を実行してください。
●パッケージがFlutterに依存していない場合は
pub run build_runner build
を実行してください。
The features
The syntax
Basics
Freezedは多くのコードジェネレータとは動作が異なります。Freezedを使ってクラスを定義するには、プロパティを宣言するのではなく、代わりにfactoryコンストラクタを宣言します。
例えば、2つのプロパティを持つPersonクラスを定義したい場合、
- String型のnameプロパティ
- int型のageプロパティ
これらのプロパティをパラメーターとして受け取るファクトリコンストラクターを定義してください。
@freezed
abstract class Person with _$Person {
factory Person({ String name, int age }) = _Person;
}
これにより、次のように書くことができます。
var person = Person(name: 'Remi', age: 24);
print(person.name); // Remi
print(person.age); // 24
注:
コンストラクターに名前付きパラメーター以外を使用することもできます。
すべての有効なパラメータ構文がサポートされています。そのため、次のように書くことができます。
@freezed
abstract class Person with _$Person {
factory Person(String name, int age) = _Person;
}
Person('Remi', 24)
↑両方とも必須パラメータのパターン。
@freezed
abstract class Person with _$Person {
const factory Person(String name, {int age}) = _Person;
}
Person('Remi', age: 24)
nameは必須パラメータ、ageは名前付きパラメータなのでオプションパラメータ。
You are also not limited to one constructor and non-generic class.
From example, you should write:
コンストラクタを複数定義することもできますし、ジェネリック型を使うことも可能です。
@freezed
class Union<T> with _$Union<T> {
const factory Union(T value) = Data<T>;
const factory Union.loading() = Loading<T>;
const factory Union.error([String? message]) = ErrorDetails<T>;
}
See unions/Sealed classes for more information.
詳しくはunions/Sealed classesをご覧ください。
The abstract
keyword
As you might have noticed, the abstract keyword is not needed anymore when declaring freezed classes.
もうお気づきの方もおられるかもしれません。freezedでクラスを宣言するときに、abstractキーワードはもう必要ありません。
This allows you to easily use mixin
s with the benefit of having your IDE telling you what to implement.
これにより、ミックスインを簡単に使用できるようになり、IDEに何を実装するかを指示させることができます。
@freezed
abstract class MixedIn with Mixin implements _$MixedIn {
MixedIn._();
factory MixedIn() = _MixedIn;
}
mixin Mixin {
int method() => 42;
}
上記を下記のように書き換えることが可能になります。
@freezed
class MixedIn with _$MixedIn, Mixin {
const MixedIn._();
factory MixedIn() = _MixedIn;
}
mixin Mixin {
int method() => 42;
}
Custom getters and methods
そのクラスのメソッド/プロパティを手動で定義したい場合があります。
しかし、あなたがやろうとするとすぐに気付くでしょう:
@freezed
abstract class Person with _$Person {
const factory Person(String name, {int age}) = _Person;
void method() {
print('hello world');
}
}
↑の方法では機能しません。
デフォルトではFreezedはクラスを拡張する方法がないからです。代わりに”implements”を使います。
これを修正するには、Freezedに正しいコードを生成させるために、少しシンタックスを変える必要があります。
(2021/4/16変更↓)
そのためには、単一のプライベートコンストラクターを次のように定義する必要があります。
@freezed
class Person with _$Person {
const Person._(); // Added constructor
const factory Person(String name, {int? age}) = _Person;
void method() {
print('hello world');
}
}
Asserts
クラス定義時の通常のユースケースとしてassert( … )文をコンストラクタで使いたいことがあります。
class Person {
Person({
String name,
int age,
}) : assert(name.isNotEmpty, 'name cannot be empty'),
assert(age >= 0);
final String name;
final int age;
}
Freezedは@Assert修飾子によりこのユースケースをサポートしています。
abstract class Person with _$Person {
@Assert('name.isNotEmpty', 'name cannot be empty')
@Assert('age >= 0')
factory Person({
String name,
int age,
}) = _Person;
}
Non-nullable #(2021/4/16時点でこのセクション全削除)
Freezedはnon-nullable(null不可型、null-safetyの話)に対応しています。
これは、null許容でない型がコンパイルされない場合は常に自動的に
assert(my_property != null);
追加することによって行われます。
つまり、Freezedを使っていて、プロパティがnullable型(null可型)の場合、それを明示的に示す必要がある、ということです。
Freezedがnon-nullable型(null不可型)と見なすもの
- 必須パラメータ
- @Defaultが使われているオプショナルpositional parameters
- @requiredが使われているnamed parameters
Freezedがnullable(null可型)と見なすもの
- オプショナルパラメータ
- @nullable修飾子のついているパラメータ
もっと具体的に説明すると、Personクラスを以下のように定義した場合
@freezed
abstract class Person with _$Person {
const factory Person(
String name, {
@required int age,
Gender gender,
}) = _Person;
}
nameとage、両方のフィールドはnon-nullable(null不可型)となります。
nameフィールドは必須パラメータなのでnon-nullableとなる。
ageフィールドはnamed parameterで@requiredが付けられているのでnon-nullableとなる。
一方で、genderフィールドはnullable(null可型)となります。
つまり、上記のようにPersonクラスを定義した場合以下のように記述できることになります。
Person('Remi', age: 24);
Person('Remi', age: 24, gender: Gender.male);
Person('Remi', age: 24, gender: null);
一方で、以下のようなコードを書くと例外が発生します。
Person(null) // name cannot be null
Person('Remi') // age cannot be null
実際にnon-nullable型へ移行する時が来たら、コードを以下のようにアップデートすることができます。
@freezed
abstract class Person with _$Person {
const factory Person(
String name, {
required int age,
Gender? gender, //←nullable型(null可型)
}) = _Person;
}
Forcing a variable to be nullable:
場合によっては、これらの(上述の)ルールに反し、たとえば、null許容の名前付き必須パラメーターが必要になることがあります。
その場合@nullable修飾子を付けるとnullable型にできます。
例えば、上述のPersonクラスでageフィールドをnullable(null可型)にしたい場合、
@freezed
abstract class Person with _$Person {
const factory Person(
String name, {
@nullable @required int age,
Gender gender,
}) = _Person;
}
↑のように書くことができます。
こう書くと、以下のようなコンストラクタ呼び出しができるようになります。
Person('Remi') // no longer throws an exception
ageフィールドは自動的にnullで初期化される。ageフィールドをnull可型にしたので例外は発生しない。
ここまで全削除。
Default values #
フィールドにデフォルト値を設定したい場合、
abstract class Example with _$Example {
const factory Example([@Default(42) int value]) = _Example;
}
上記のように書くことでデフォルト値を設定できます。
NOTE:
If you are using serialization/deserialization, this will automatically add a @JsonKey(defaultValue: <something>)
for you.
Late #
Freezed also supports the late
keyword.
Freezedはlateキーワードもサポートしています。
If you are unfamiliar with that keyword, what late does is it allows variables to be lazily initialized.
lateキーワードは、フィールドの遅延初期化を可能にするために使うキーワードです。
You may be familiar with such syntax:
あなたは下記のような構文に精通しているかもしれません:
Model _model;
Model get model => _model ?? _model = Model();
With Dart’s late keyword, we could instead write:
Dartのlateキーワードを使用すると、代わりに次のように書くことができます。
late final model = Model();
And with Freezed, we could write:
late final model = Model();
Since Freezed relies on immutable classes only, then this may be very helpful for computed properties.
Freezedはimmutable(不変な)クラスのみに依存しているため、これはcomputedプロパティに非常に役立つ場合があります。
abstract class Todos with _$Todos {
factory Todos(List<Todo> todos) = _Todos;
late final completed = todos.where((t) => t.completed).toList();
}
0.14.0 and null-safety
Important note:
From 0.14.0 and onwards, Freezed does not support non-null-safe code.
If you want to keep using Freezed but cannot migrate to null-safety yet, use the version 0.12.7 instead.
Note that this version is no-longer maintained (so bugs found there won’t be fixed).
For the documentation of the version 0.12.7, refer to https://pub.dev/packages/freezed/versions/0.12.7
In the scenario where you are using the version 0.12.7, but one of your dependency is using 0.14.0 or above, you will have a version conflict on freezed_annotation
.
In that case, you can fix the error by adding the following to your pubspec.yaml
:
dependency_overrides:
freezed: ^0.12.7
freezed_annotation: ^0.12.0
Motivation
While there are many code-generators available to help you deal with immutable objects, they usually come with a trade-off.
Either they have a simple syntax but lack features, or they have very advanced features but with complex syntax.
A typical example would be a “clone” method.
Current generators have two approaches:
- a
copyWith
, usually implemented using??
:MyClass copyWith({ int? a, String? b }) { return MyClass(a: a ?? this.a, b: b ?? this.b); }
The syntax is very simple to use, but doesn’t support some use-cases: nullable values.
We cannot use suchcopyWith
to assignnull
to a property like so:person.copyWith(location: null)
- a builder method combined with a temporary mutable object, usually used this way:
person.rebuild((person) { return person ..b = person; })
The benefits of this approach are that it does support nullable values.
On the other hand, the syntax is not very readable and fun to use.
Say hello to Freezed~, with support for advanced use-cases without compromising on the syntax.
See the example or the index for a preview on what’s available
Index
- 0.14.0 and null-safety
- Motivation
- Index
- How to use
- The features
- Utilities
How to use
Install
To use Freezed, you will need your typical build_runner/code-generator setup.
First, install build_runner and Freezed by adding them to your pubspec.yaml
file:
# pubspec.yaml
dependencies:
freezed_annotation:
dev_dependencies:
build_runner:
freezed:
This installs three packages:
- build_runner, the tool to run code-generators
- freezed, the code generator
- freezed_annotation, a package containing annotations for freezed.
Run the generator
Like most code-generators, Freezed will need you to both import the annotation (meta) and use the part
keyword on the top of your files.
As such, a file that wants to use Freezed will start with:
import 'package:freezed_annotation/freezed_annotation.dart';
part 'my_file.freezed.dart';
CONSIDER also importing package:flutter/foundation.dart
.
The reason being, importing foundation.dart
also imports classes to make an object nicely readable in Flutter’s devtool.
If you import foundation.dart
, Freezed will automatically do it for you.
A full example would be:
// main.dart
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';
part 'main.freezed.dart';
@freezed
class Union with _$Union {
const factory Union(int value) = Data;
const factory Union.loading() = Loading;
const factory Union.error([String? message]) = ErrorDetails;
}
From there, to run the code generator, you have two possibilities:
flutter pub run build_runner build
, if your package depends on Flutterpub run build_runner build
otherwise
Ignore lint warnings on generated files
It is likely that the code generated by Freezed will cause your linter to report warnings.
The solution to this problem is to tell the linter to ignore generated files, by modifying your analysis_options.yaml
:
analyzer:
exclude:
- "**/*.g.dart"
- "**/*.freezed.dart"
The features
The syntax
Basics
Freezed works differently than most generators. To define a class using Freezed, you will not declare properties but instead factory constructors.
For example, if you want to define a Person
class, which has 2 properties:
- name, a
String
- age, an
int
To do so, you will have to define a factory constructor that takes these properties as parameter:
@freezed
class Person with _$Person {
factory Person({ String? name, int? age }) = _Person;
}
Which then allows you to write:
var person = Person(name: 'Remi', age: 24);
print(person.name); // Remi
print(person.age); // 24
NOTE:
You do not have to use named parameters for your constructor.
All valid parameter syntaxes are supported. As such you could write:
@freezed
class Person with _$Person {
factory Person(String name, int age) = _Person;
}
Person('Remi', 24)
@freezed
class Person with _$Person {
const factory Person(String name, {int? age}) = _Person;
}
Person('Remi', age: 24)
…
You are also not limited to one constructor and non-generic class.
From example, you should write:
@freezed
class Union<T> with _$Union<T> {
const factory Union(T value) = Data<T>;
const factory Union.loading() = Loading<T>;
const factory Union.error([String? message]) = ErrorDetails<T>;
}
See unions/Sealed classes for more information.
The abstract
keyword
As you might have noticed, the abstract keyword is not needed anymore when declaring freezed
classes.
This allows you to easily use mixin
s with the benefit of having your IDE telling you what to implement.
We can now turn this:
@freezed
abstract class MixedIn with Mixin implements _$MixedIn {
MixedIn._();
factory MixedIn() = _MixedIn;
}
mixin Mixin {
int method() => 42;
}
into this:
@freezed
class MixedIn with _$MixedIn, Mixin {
const MixedIn._();
factory MixedIn() = _MixedIn;
}
mixin Mixin {
int method() => 42;
}
Custom getters and methods
Sometimes, you may want to manually define methods/properties on that class.
But you will quickly notice that if you try to do:
@freezed
class Person with _$Person {
const factory Person(String name, {int? age}) = _Person;
void method() {
print('hello world');
}
}
then it won’t work.
This is because by default, Freezed has no way of “extending” the class and instead “implements” it.
To fix it, we need a subtle syntax change to allow Freezed to generate valid code.
To do so, we have to define a single private constructor:
@freezed
class Person with _$Person {
const Person._(); // Added constructor
const factory Person(String name, {int? age}) = _Person;
void method() {
print('hello world');
}
}
Asserts
A common use-case with classes is to want to add assert(...)
statements to a construtor:
class Person {
Person({
String? name,
int? age,
}) : assert(name.isNotEmpty, 'name cannot be empty'),
assert(age >= 0);
final String name;
final int age;
}
Freezed supports this use-case through the @Assert
decorator:
abstract class Person with _$Person {
@Assert('name.isNotEmpty', 'name cannot be empty')
@Assert('age >= 0')
factory Person({
String? name,
int? age,
}) = _Person;
}
Default values
Unfortunately, Dart does not allow constructors with the syntax used by Freezed to specify default values.
Which means you cannot write:
abstract class Example with _$Example {
const factory Example([int value = 42]) = _Example;
}
But Freezed offers an alternative for this: @Default
As such, you could rewrite the previous snippet this way:
abstract class Example with _$Example {
const factory Example([@Default(42) int value]) = _Example;
}
NOTE:
If you are using serialization/deserialization, this will automatically add a @JsonKey(defaultValue: <something>)
for you.
Late
Freezed also supports the late
keyword.
If you are unfamiliar with that keyword, what late
does is it allows variables to be lazily initialized.
You may be familiar with such syntax:
Model _model;
Model get model => _model ?? _model = Model();
With Dart’s late
keyword, we could instead write:
late final model = Model();
And with Freezed, we could write:
late final model = Model();
Since Freezed relies on immutable classes only, then this may be very helpful for computed properties.
For example, you may write:
abstract class Todos with _$Todos {
factory Todos(List<Todo> todos) = _Todos;
late final completed = todos.where((t) => t.completed).toList();
}
As opposed to a normal getter, this will cache the result of completed
, which is more efficient.
通常のゲッターとは対照的に、これは完了の結果をキャッシュするので、より効率的です。
NOTE:
Getters decorated with late
will also be visible on the generated toString
.
注意:
lateのついたゲッターは、生成されたtoStringからもアクセスできます。
Constructor tear-off
工事中🏗
Decorators and comments #
工事中🏗
Mixins and Interfaces for individual classes for union types #
When you have multiple types in the same class, you might want to make one of those types to implement a interface or mixin a class.
同じクラスに複数のタイプがある場合は、それらのタイプの1つを作成して、インターフェースを実装したり、クラスをミックスインしたりすることができます。
You can do that using the @Implements decorator or @With respectively. In this case City is implementing with GeographicArea.
これは、それぞれ@Implementsデコレータまたは@Withを使用して行うことができます。 下記の場合、CityはGeographicAreaを実装しています。
abstract class GeographicArea {
int get population;
String get name;
}
@freezed
class Example with _$Example {
const factory Example.person(String name, int age) = Person;
@Implements(GeographicArea)
const factory Example.city(String name, int population) = City;
}
In case you want to specify a generic mixin or interface you need to declare it as a string using the With.fromString constructor, Implements.fromString respectively.
汎用ミックスインまたはインターフェイスを指定する場合は、With.fromStringコンストラクターImplements.fromStringをそれぞれ使用して文字列として宣言する必要があります。
下記のサンプルでStreetクラスはAdministrativeArea<House>をミックスインしています。
abstract class GeographicArea {}
abstract class House {}
abstract class Shop {}
abstract class AdministrativeArea<T> {}
@freezed
class Example with _$Example {
const factory Example.person(String name, int age) = Person;
@With.fromString('AdministrativeArea<House>')
const factory Example.street(String name) = Street;
@With(House)
@Implements(Shop)
@Implements(GeographicArea)
const factory Example.city(String name, int population) = City;
}
CityクラスはHouseクラスをミックスインし、なおかつShopクラスとGeographicAreaクラスをimplementsしている、ということだと思われる。
In case you want to make your class generic, you do it like this:
クラスをジェネリック型にしたい場合は、次のようにします。
@freezed
class Example<T> with _$Example<T> {
const factory Example.person(String name, int age) = Person<T>;
@With.fromString('AdministrativeArea<T>')
const factory Example.street(String name, T value) = Street<T>;
@With(House)
@Implements(GeographicArea)
const factory Example.city(String name, int population) = City<T>;
}
Note: You need to make sure that you comply with the interface requirements by implementing all the abstract members. If the interface has no members or just fields, you can fulfil the interface contract by adding them in the constructor of the union type. Keep in mind that if the interface defines a method or a getter, that you implement in the class, you need to use the Custom getters and methods instructions.
implementsするためにメンバを実装する場合Custom getters and methodsの箇所に書いてあることに気を付ける必要がある。
Note 2: You cannot use @With/@Implements with freezed classes. Freezed classes can neither be extended nor implemented.
注2:freezedで生成したクラスに@ With / @Implementsを使用することはできません。 freezedで生成したクラスは、拡張も実装もできません。
==/toString #
Freezedを使うと、toStringメソッド、hashCode、==演算子は期待通りにoverrideされます。
@freezed
abstract class Person with _$Person {
factory Person({ String name, int age }) = _Person;
}
//上記の宣言だけで、下記のような挙動になりますよ、ということ。
void main() {
print(Person(name: 'Remi', age: 24)); // Person(name: Remi, age: 24)
print(
Person(name: 'Remi', age: 24) == Person(name: 'Remi', age: 24),
); // true
}
copyWith #
工事中🏗
Freezedにより生成されたcopyWithメソッドは、null値のセットをサポートしています。
例えば、下記のようなPersonクラスの宣言により、
@freezed
abstract class Person with _$Person {
factory Person(String name, int age) = _Person;
}
下記のように書けます。
var person = Person('Remi', 24);
// `age` not passed, its value is preserved
print(person.copyWith(name: 'Dash')); // Person(name: Dash, age: 24)
// `age` is set to `null`
print(person.copyWith(age: null)); // Person(name: Remi, age: null)
ageフィールドにnull値を渡した場合に、ageフィールドの値がnullであるPersonインスタンスを返していることに注目してください。
Deep copy #
While copyWith is very powerful in itself, it starts to get inconvenient on more complex objects.
copyWithはそれ自体が非常に強力ですが、より複雑なオブジェクトでは不便になり始めます。
@freezed
class Company with _$Company {
factory Company({String? name, Director? director}) = _Company;
}
@freezed
class Director with _$Director {
factory Director({String? name, Assistant? assistant}) = _Director;
}
@freezed
class Assistant with _$Assistant {
factory Assistant({String? name, int? age}) = _Assistant;
}
Then, from a reference on Company, we may want to perform changes on Assistant.
For example, to change the name of an assistant, using copyWith we would have to write:
次に、会社の参照から、アシスタントを変更したい場合があるかもしれません。
たとえば、アシスタントの名前を変更するには、copyWithを使用して次のように記述する必要があります。
Company company;
Company newCompany = company.copyWith(
director: company.director.copyWith(
assistant: company.director.assistant.copyWith(
name: 'John Smith',
),
),
);
This works, but is relatively verbose with a lot of duplicates.
This is where we could use Freezed’s “deep copy”.
これは機能しますが、重複が多く、比較的冗長です。
ここで、Freezedの「ディープコピー」を使用できます。
If an object decorated using @freezed contains other objects decorated with @freezed, then Freezed will offer an alternate syntax to the previous example:
@freezedで生成したオブジェクトに、@ freezedで生成した他のオブジェクトが含まれている場合、Freezedは前の例の代替構文を提供します↓。
Company company;
Company newCompany = company.copyWith.director.assistant(name: 'John Smith');
This snippet will achieve strictly the same result as the previous snippet (creating a new company with an updated assistant name), but no longer has duplicates.
このスニペットは、前のスニペット(更新されたアシスタント名で新しい会社を作成する)と厳密に同じ結果を達成しますが、重複はなくなります。
Going deeper in this syntax, if instead, we wanted to change the director’s name then we could write:
この構文をさらに深く掘り下げて、代わりにディレクターの名前を変更したい場合は、次のように記述できます。
Company company;
Company newCompany = company.copyWith.director(name: 'John Doe');
Overall, based on the definitions of Company/Director/Assistant mentioned above, all the following “copy” syntaxes will work:
全体として、上記の会社/ディレクター/アシスタントの定義に基づいて、次のすべての「コピー」構文が機能します。
Company company;
company = company.copyWith(name: 'Google', director: Director(...));
company = company.copyWith.director(name: 'Larry', assistant: Assistant(...));
company = company.copyWith.director.assistant(name: 'John', age: 42);
Null consideration
Some objects may can also be null
. For example, using our Company
class, then Director
may be null
.
As such, writing:
一部のオブジェクトはnullになることもあります。 たとえば、前述のCompanyクラスにおいて、Directorがnullになる場合があります。
そのため、次のように記述します。
Company company = Company(name: 'Google', director: null);
Company newCompany = company.copyWith.director.assistant(name: 'John');
doesn’t make sense.
意味がありません。
We can’t change the director’s assistant if there is no director to begin with.
そもそもdirectorがないと、directorのassistantを変えることはできません。
In that situation, company.copyWith.director will return null, and our previous example will result in a null exception.
そのような状況では、company.copyWith.directorはnullを返し、前の例ではnull例外が発生します。
To fix it, we can use the ?.
operator and write:
これを修正するためには、?.演算子を使用しましょう。
Company? newCompany = company.copyWith.director?.assistant(name: 'John');
参考
https://pub.dev/packages/freezed