Freezed 3.0 マイグレーション検討
待望の Freezed 3.0 リリース 🥳
ついに来ました!Freezed 3.0 がリリースされました 🥳
本記事では、Freezed 3.0 のマイグレーション方針と、アップデート後に追加された機能について見ていこうと思います 👀
Freezed 3.0 へのアップデートに備える
早速アップデートするぞ!!!と言いたいところですが、開発中のプロジェクトにおいては下記の事情によりアップデートできない場合もあることでしょう🧊
- Flutter 3.29.0 にアップデートしていない / アップデート様子見している
- 一部パッケージの競合が解決できない
- when/map バリアントがプロジェクト内で多用されている
そこで、Freezed 3.0 へのマイグレーション方針として、あらかじめやっておくべきことを整理しておきましょう。
想定するマイグレーション環境
- Flutter 3.27.x / Dart 3.6.0 => Flutter 3.29.0 / Dart 3.7.0
- Freezed 2.5.8 => Freezed 3.0.x
上記を仮定してマイグレーションを検討します。
Flutter / Dart のアップデート
Freezed 3.0 にアップデートするには、Flutter 3.29.0 / Dart 3.7.0 へのアップデートが必須となっています。
各種パッケージの競合解決
主に analyzer
, source_gen
のバージョンで他のパッケージと競合することが多いと思われます。
主に dev_dependencies にあるコード自動生成系のパッケージについて、あらかじめ最新版に更新して依存関係を解決しておきましょう!
sealed / abstract が必須になった
ファクトリコンストラクターを使用するクラスには、 sealed
/abstract
が必要になりました:
@freezed
-class Person with _$Person {
+abstract class Person with _$Person {
const factory Person({
required String firstName,
required String lastName,
required int age,
}) = _Person;
factory Person.fromJson(Map<String, Object?> json)
=> _$PersonFromJson(json);
}
@freezed
-class Model with _$Model {
+sealed class Model with _$Model {
factory Model.first(String a) = First;
factory Model.second(int b, bool c) = Second;
}
既存のユニオン型のクラスについては予め、sealed
を適用しておくとよいでしょう 🙏
ユニオンケースのパターンマッチング時の網羅性を高めることにも繋がります。
when/map バリアントをリプレイスする
おそらく古いプロジェクトでFreezed 3.0 マイグレする場合に一番大変になる作業はこちらです。
Freezed 3.0 で Freezed ユニオン型 の when/map バリアントが撤去されます。
プロジェクト内で Freezed クラスの when/map バリアントを利用している場合は、下記のようにswitch 式, if-case 式への置き換えを頑張って進めておきましょう:
switch 文への置き換えの例
/// ユニオン型のサンプルクラス
sealed class Example with _$Example {
factory Example.data(int data) = ExampleData;
factory Example.error(Object error) = ExampleError;
}
// BEFORE 🙅 ※Freezed 3.0 で使えなくなります
obj.map(
data: (example) => print(example.data),
error: (example) => print(example.error),
);
obj.when(
data: (data) => print(data),
error: (error) => print(error),
);
obj.maybeWhen(
data: (data) => print(data),
orElse: () => print('default'),
);
// AFTER 🙆
switch (obj) {
case ExampleData v: print(v.data);
case ExampleError v: print(v.error);
}
switch (obj) {
case ExampleData(:final data): print(data);
case ExampleError(:final error): print(error);
}
switch (obj) {
case ExampleData(:final data): print(data);
default: print('default');
}
switch式への置き換えの例
// BEFORE 🙅 ※Freezed 3.0 で使えなくなります
final result = obj.map(
data: (example) => '${example.data}',
error: (example) => '${example.error}',
);
final result = obj.when(
data: (data) => '$data',
error: (error) => '$error',
);
final result = obj.maybeWhen(
data: (data) => '$data',
orElse: () => 'default',
);
// AFTER 🙆
final result = switch (obj) {
ExampleData v => '${v.data}',
ExampleError v => '${v.error}',
};
final result = switch (obj) {
ExampleData(:final data) => '$data',
ExampleError(:final error) => '$error',
};
final result = switch (obj) {
ExampleData(:final data) => '$data',
_ => 'default',
};
参考: https://dart.dev/language/branches#switch-expressions
if-case 式への置き換えの例
// BEFORE 🙅 ※Freezed 3.0 で使えなくなります
obj.maybeWhen(
data: (data) => print(data),
orElse: () => print('default'),
);
// AFTER 🙆
if (obj case ExampleData(:final data)) {
print(data);
} else {
print('default');
}
参考: https://dart.dev/language/branches#if-case
when/map バリアント生成の無効化
また、リプレイス後や新規プロジェクトではうっかり使ってしまわないように build.yaml
の freezed::option で when/map バリアントの生成を無効化しておくことをおすすめします。個別に無効化する設定もできるため、段階的にリプレイスをすすめるときにも活用できます:
targets:
$default:
builders:
freezed:
options:
format: false
# map, mapOrNull, maybeMap の生成をまとめて無効化
map: false
# whenOrNull のみ無効化
when:
when: true
maybe_when: true
when_or_null: false
Freezed 3.0 アップデート後の作業予定
build.yaml の when/map バリアント設定の撤去
targets:
$default:
builders:
freezed:
options:
# Freezedに対して、.freezed.dartファイルをフォーマットしないように指示する
# これにより、コード生成の速度が大幅に向上する可能性がある
format: false
# プロジェクト全体で copyWith/== の生成を無効にするかどうかの設定
# copy_with: false
# equal: false
when/map で個別に設定していた設定は使用しなくなるため撤去しておきましょう。
また、必要に応じてformat を無効化したり、不要なコードの生成を無効化することもできます。
新構文の理解とチームメンバーへの周知
Freezed 3.0 でデータクラスを生成する2つの構文が利用できます:
- Primary constructors (プライマリコンストラクタ)
- Classic classes (クラシッククラス)
今後の開発において、どちらの構文を採用するかを決定してチーム内に周知しておく必要があるでしょう。
プライマリコンストラクタ
従来の構文がこちらです。
Freezed は factory
コンストラクタに依存してプライマリコンストラクタを実装します。つまり、 factory を定義すれば、残りのすべては Freezed が自動生成してくれるというアイデアです:
import 'package:freezed_annotation/freezed_annotation.dart';
// 必須: Freezedによって自動生成されるコードを 'main.dart' に関連付ける
part 'main.freezed.dart';
// オプション: Person クラスをシリアライズ可能にする場合にのみ追加
part 'main.g.dart';
abstract class Person with _$Person {
const factory Person({
required String firstName,
required String lastName,
required int age,
}) = _Person;
factory Person.fromJson(Map<String, Object?> json)
=> _$PersonFromJson(json);
}
クラシッククラス
こちらが新しく追加された構文になります。
プライマリコンストラクタの代わりに、通常のDartクラスを書きます。
この構文では、通常のコンストラクタとフィールドの組み合わせを記述します:
import 'package:freezed_annotation/freezed_annotation.dart';
// 必須: Freezedによって自動生成されるコードを 'main.dart' に関連付ける
part 'main.freezed.dart';
// オプション: Person クラスをシリアライズ可能にする場合にのみ追加
part 'main.g.dart';
()
abstract class Person with _$Person {
const Person({
required this.firstName,
required this.lastName,
required this.age,
});
final String firstName;
final String lastName;
final int age;
factory Person.fromJson(Map<String, Object?> json)
=> _$PersonFromJson(json);
Map<String, Object?> toJson() => _$PersonToJson(this);
}
この構文において、Freezed は copyWith
/toString
/==
/hashCode
を生成しますが、JSONエンコーディングに関することは何も行いません。したがって、それが必要な場合にはマニュアルで @JsonSerializable
を追加する必要があります。
クラシッククラスでは、クラスの継承や定数でないデフォルト値を指定できるようになるなど、高度なコンストラクタロジックを可能にする利点があります。
プライマリコンストラクタで新たにできるようになったこと
クラシッククラスではなく、引き続き、プライマリコンストラクタを採用する場合、Freezed 3.0 からできるようになったことを確認しておきます。
デフォルト値に定数以外を指定する
以前のプライマリコンストラクタで @Default
を使用する場合には定数値しか指定することができないという制約がありました。
定数値以外を使用する場合は下記の2つの方法があります:
- プライマリコンストラクタを使うのをやめる(クラシッククラスに移行する)
-
MyClass._()
コンストラクタを追加して値を初期化する
後者は特に大規模なモデルを書くときに便利で、デフォルト値を与えるためだけに多くのコードを書く必要がなくなります。
一例を挙げます:
sealed class Response<T> with _$Response<T> {
// デフォルト定数値ではない "time" パラメータ
Response._({DateTime? time}) : time = time ?? DateTime.now();
// コンストラクタは ._() にパラメータを渡すことができる
factory Response.data(T value, {DateTime? time}) = ResponseData;
// ._ parameters が名前付きでオプショナルの場合, factory コンストラクタはそれを指定する必要がない
factory Response.error(Object error) = ResponseError;
final DateTime time;
}
この例では、time
フィールドのデフォルト値に DateTime.now()
を与えられるようになります 🎉
クラスを拡張する
Freezedクラスに別のクラスを継承させたい場合もあるでしょうが、残念ながら、factory
コンストラクタでは super(...)
を指定することができません。
ただし、MyClass._()
を再度指定することでこれを回避することができます。
以下はその例です:
class Subclass {
Subclass.name(this.value);
final int value;
}
class MyFreezedClass extends Subclass with _$MyFreezedClass {
// コンストラクタでパラメータを受け取り、`super.field` で参照することができる
MyFreezedClass._(super.value): super.name();
factory MyFreezedClass(int value, /* other fields */) = _MyFreezedClass;
}
その他
ユニオン型のクラスをマニュアルで追加する
モデルをきめ細かく制御するために、Freezed は ユニオン型のサブクラスをマニュアルで記述する機能を提供しています。
下記の例で考えてみましょう:
class Result<T> with _$Result {
factory Result.data(T data) = ResultData;
factory Result.error(Object error) = ResultError;
}
sealed
では、上記の例でResultData
クラスを自前で書きたいとします。
その場合は、同じファイルに ResultData
クラスを定義すればよいだけです:
class Result<T> with _$Result {
factory Result.data(T data) = ResultData;
factory Result.error(Object error) = ResultError;
}
class ResultData<T> implements Result<T> {
// TODO: Result<T> を実装する
}
sealed
この自前定義のクラスは Freezed クラスにすることもできます。
class Result<T> with _$Result {
Result._();
factory Result.data(T data) = ResultData;
factory Result.error(Object error) = ResultError;
}
class ResultData<T> extends Result<T> {
factory ResultData(T data) = _ResultData;
// TODO: ResultData 固有のメソッドを追加する
}
sealed
モデルをきめ細かく制御するのに役立つでしょう。
まとめ
Freezed 3.0 へのアップデートに向けて、いくつかのマイグレーション作業が必要です:
- 🆙 Flutter 3.29.0 へのアップデート
- 🆙 パッケージ競合解消
- 🔁 when/map バリアントのリプレイス作業
アップデート後は、チーム内でプライマリコンストラクタとクラシッククラスのどちらを採用するかを決定した上で、プロジェクト内のコードの整備を進めていきましょう 🙏
Discussion