シ〜らかんす

プログラミングとか、カメラとか。

【flutter freezedライブラリ】copyWithの引数にnullを渡す時の挙動に注意しよう

概要

flutterのfreezedライブラリを使ってモデルを定義し、コードを生成するとimmutableなオブジェクトを定義してくれる。 このオブジェクトには、copyWithというメソッドが付いており、引数で与えられた属性値でオブジェクトを初期化して返してくれるのだが、1点気をつけるべき挙動がある。

具体的には、このcopyWithにnullを渡しても、属性値はnullになって返されるのではなく、copyWithを呼び出す前の状態のままになる、ということだ(2024年2月現在、freezedバージョン2.4.5)。

「copyWithの属性値がnullである」=「その属性値をnullにして初期化する」のではなく、 「copyWithの属性値がnullである」=「その属性値は置き換えない」という挙動になっているのである。

具体例

例えば、以下のようにモデルを定義する。

@freezed
class SampleModel<T extends Object> with _$SampleModel<T> {
  const factory SampleModel({
    T? value,
  }) = _SampleModel<T>;
}

copyWithを使って、valueにnullを入れるとする。

SampleModel model = SampleModel(value: 10);
model = model.copyWith(value: null);
print(model.valule);

出力されるのはなんと 10 のままになる。

freezedで生成されたファイルを確認

freezedで生成されたファイル中の、copyWithメソッドの実装部分を見てみると、次のようになっている。

/// @nodoc
class _$SampleModelCopyWithImpl<T extends Object, $Res,
    $Val extends SampleModel<T>> implements $SampleModelCopyWith<T, $Res> {
  _$SampleModelCopyWithImpl(this._value, this._then);

  // ignore: unused_field
  final $Val _value;
  // ignore: unused_field
  final $Res Function($Val) _then;

  @pragma('vm:prefer-inline')
  @override
  $Res call({
    Object? value = null,
  }) {
    return _then(_value.copyWith(
      value: null == value
          ? _value.value
          : value // ignore: cast_nullable_to_non_nullable
              as T?,
    ) as $Val);
  }
}

valueがnullの場合、引数で与えられたvalueではなく、元の値(_value.value)を使っていることがわかる。

対処方法

  1. この仕様を理解して、null値で置き換えたいときは、copyWithを使わずに初期化する。
SampleModel model = SampleModel(value: 10);
model = SampleModel(value: null);
print(model.value);

この方法だと属性値の多いクラスの記述がやたらと長くなってしまうし、チームで開発していると他メンバーが混乱しかねない。

  1. 値オブジェクトを定義し、「空である」という状態を定義する
@freezed
class SampleModel with _$SampleModel {
  const factory SampleModel({
    SampleValue value,
  }) = _SampleModel;
}

@freezed
class SampleValue<T extends Object> with _$SampleValue<T> {
  const factory SampleValue({
    T? value,
  }) = _SampleValue<T>;

  factory SampleValue.empty() => const SampleValue(value: null);

  bool isEmpty() {
    return value == null;
  }
}

この方法では、「空である」状態を表すことができる値オブジェクトのクラスを作り、nullを使わないようにする。 個人的にはこちらのやり方が気に入っている。コード量はやや増えるものの、copyWithにnullを渡してハマるリスクを避けつつ、意図も明確に示せる。

参考リンク

github.com