概要
どうも、@daiki1003です!この記事は、Qiita Advent Calendar Flutter6日目の記事です。
中規模以上のアプリになると多分ほぼほぼ欠かせない存在になっているであろうfreezed
。
でも色んなannotationのパラメタがあったりrequired
付けたらどうなるのか。
今回の記事はそんな疑問にお答えする形で進めていければと思っています。
もし、他にも分からないことがあればTwitterでぜひ聴いてもらえると嬉しいです!
それでは行ってみましょう!
筆者環境
freezed
: 0.15.0+1freezed_annotation
: 0.15.0
※これを調べていて、1.0.0にアップデートしていないことに気がついたので
もしアップデートで何か下記内容に変更があれば更新します!
required?nullable?unknownEnum?
ある程度、使い慣れて最初にぶつかった壁というか疑問はこれでした。
・なんかとりあえずで required
とか?
とか付けてたけど実際どういう風に関係してるのか分からん。
・unknownEnum
やdefaultValue
との関係性は?
requiredって何のために指定しているの?
少し雑な言い方になってしまうのですが、ズバリ
初期値を指定しなくても済む様にしているだけ
と思ってもらってOKだと思います。
@freezed class Hoge { const factory Hoge({ required String param, }); factory Hoge.fromJson(Map<String, dynamic> json) => _$HogeFromJson(json); }
これはOKですが、required
を外すと生成に失敗します。
「nullでもない、デフォルト値もない変数があってもいいだろう」とぺこぱのように許してくれたりはしません。
nullable
にしたり、@Default('')
などを指定することで
値が安定するのでfreezedファイルが生成可能になります。
つまり、言い換えるとnon-nullable
なパラメタに対して、json
に含まれるからそれを使ってね、ということになります。
requiredを指定したパラメタは、json内に該当するキーが必ず必要というだよね?
答えは、 必ずしもYESではない です。
ちょっと驚いた人もいると思います。
かくいう僕も、最初はそういう意味だと思っていました。
例として、先ほどのparam
をnullable
にしたクラスを考えてみましょう。
@freezed class Hoge { const factory Hoge({ required String? param, }); factory Hoge.fromJson(Map<String, dynamic> json) => _$HogeFromJson(json); }
ここで、hoge.freezed.dart
を覗いてみましょう。
_Hoge call({required String? param}) { return _Hoge( param: param, ); }
required
の指定の有無はこのnamed parameters
のrequired
の有無を意味します。
つまり、 コンストラクタ経由でインスタンスを生成する際にそのパラメタの指定が必須 という意味になります。
次に、hoge.g.dart
を覗いてみます。
_$_Hoge _$$_HogeFromJson(Map<String, dynamic> json) => _$_Hoge( param: json['param'] as String?, );
となっており、required
はここに一切の関与をしていません。
仮にparam
キーが存在しなくとも、param
にはnull
が入るだけです。
なので、required
を指定したパラメタはjson
に存在しなくとも問題ないということになります。
(もちろん、non-nullable
な場合は必要です)
さらに。
freezed
から生成されるクラスは、APIレスポンスのパース時に使われる場合も多いのではないかと思います。
その際は、基本的に Hoge.fromJson(response)
の様な形でパースするため、コンストラクタを直で使うことはないでしょう。
つまり、このような利用用途においてはrequired
の有無はどちらでも良いということになります。
意外とrequired
が担っている役割が大したことないなー🤔と言う感想になりませんか?
余談ですが、もしキーが必ず存在していることを保証したい場合は、@JsonKey(required: true)
を指定します。
enumを定義したけど、知らない値の場合はどうすれば良いの?
さて、次にenumについて考えてみましょう。
APIレスポンスなどでenumを使った場合、サーバサイドの変更により未知の値をパースしなければいけない場合があります。
そんな時は、@JsonKey(unknownEnumValue:)
を使いましょう。
enum Fuga { foo, bar, }
があるとします。
これを含むHoge
クラスを考えてみます。
@freezed class Hoge { const factory Hoge({ required Fuga fuga, }); factory Hoge.fromJson(Map<String, dynamic> json) => _$HogeFromJson(json); }
生成すると、 hoge.g.dart
に
const _$FugaEnumMap = { Fuga.foo: 'foo', Fuga.bar: 'bar', };
の様なMap
が定義されます。
これを利用して何をやるかは、自明かと思いますので割愛します。
この時にjson
に'buz'
が含まれているとどうなるでしょう?
パースエラーとなり処理が止まってしまいます。
════════ Exception caught by gesture ═══════════════════════════════════════════ Invalid argument(s): `buz` is not one of the supported values: foo, bar ════════════════════════════════════════════════════════════════════════════════
そんな時は、 @JsonKey(unknownEnumValue:)
を使いましょう。
該当するenumがない時にnull
にして欲しい場合は、JsonKey.nullForUndefinedEnumValue
を指定します。
必要があれば、Fuga.unknown
などを追加します。
@freezed class Hoge { const factory Hoge({ @JsonKey(unknownEnumValue: Fuga.unknown) required Fuga fuga, }); factory Hoge.fromJson(Map<String, dynamic> json) => _$HogeFromJson(json); }
これで未知の値に対しては、パースエラーは出ずFuga.unknown
として処理が進められます。
nullableにしたら知らない値があっても大丈夫?
答えはNOです。
@freezed class Hoge { const factory Hoge({ required Fuga? fuga, }); factory Hoge.fromJson(Map<String, dynamic> json) => _$HogeFromJson(json); }
hoge.g.dart
を見てみましょう。
_$_Hoge _$$_HogeFromJson(Mapjson) => _$_Hoge( fuga: $enumDecodeNullable(_$FugaEnumMap, json['fuga']), );
と、$enumDecodeNullable
を使ってパースされていることが確認できます。
そこで、$enumDecodeNullable
の中身を見てみると
K? $enumDecodeNullable<K extends Enum, V>( Map<K, V> enumValues, Object? source, { Enum? unknownValue, }) { if (source == null) { return null; } for (var entry in enumValues.entries) { if (entry.value == source) { return entry.key; } } if (unknownValue == JsonKey.nullForUndefinedEnumValue) { return null; } if (unknownValue == null) { throw ArgumentError( '`$source` is not one of the supported values: ' '${enumValues.values.join(', ')}', ); } if (unknownValue is! K) { throw ArgumentError.value( unknownValue, 'unknownValue', 'Must by of type `$K` or `JsonKey.nullForUndefinedEnumValue`.', ); } return unknownValue; }
となっており、キーが見つからなければunknownValue == null
のif文の中の処理によりエラーが投げられてしまうためパースエラーとなってしまいます。
じゃぁ、defaultValueがあったら知らない値でも大丈夫?
これも、残念ながら答えはNOです。
@freezed class Hoge { const factory Hoge({ @JsonKey(defaultValue: Fuga.foo) required Fuga fuga, }); factory Hoge.fromJson(Map<String, dynamic> json) => _$HogeFromJson(json); }
hoge.g.dart
を見てみましょう。
_$_Hoge _$$_HogeFromJson(Mapjson) => _$_Hoge( fuga: $enumDecodeNullable(_$FugaEnumMap, json['fuga']) ?? Fuga.foo, );
となっており、先ほどの$enumDecodeNullable
がnullを返した時の値として使われていることがわかると思います。
ですが、先ほども見たように知らない値の場合はArgumentError
が投げられてしまうのでこれも望んだ挙動にはならないことになります。
いかがでしたでしょうか?
本当であれば、 ポリモーフィズムをfreezed
で実現するというところまで書きたかったのですが
想定外に長くなってしまったのでまたの機会としたいと思います。
Twitterフォローお願いします
「次回以降も記事を読んでみたい!」「この辺分からなかったから質問したい!」
そんな時は、是非Twitter (@daiki1003)やInstagram (@ashdik_flutter)のフォローお願いします♪
Twitterコミュニティ参加お願いします
Twitterコミュニティ「Flutter lovers」を開設しました!参加お待ちしております😁
☕️ Buy me a coffee
また、記事がとても役に立ったと思う人はコーヒーを奢っていただけると非常に嬉しいです!
コメント