Freezedで生成したUnionをRiverpod_generatorで使用しようとしたときにgファイルが生成されない問題

エラー概要

ある日、こんなエラーに遭遇しました。

freezedで生成したsealedクラスをriverpod_generatorのクラスで使用した際に、partファイルが生成されなかったという問題です。
dart run build_runner build -vでこんな感じのエラー文

riverpod_generator 
E InvalidTypeException: The type is invalid and cannot be converted to code.

実際のクラス、Freezedでごく普通にunionクラスを作っているのですが、

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:rerurate_flutter_template/domain/value_object/mapping_component_coordinates.dart';
import 'package:rerurate_flutter_template/domain/value_object/mapping_component_id.dart';
 
part 'mapping_components.freezed.dart';
 
@freezed
sealed class MappingComponents with _$MappingComponents {
  const MappingComponents._();
 
  const factory MappingComponents.textBlock({
    required MappingComponentId id,
    required MappingComponentCoordinates coordinates,
    required String value,
    required double fontSize,
  }) = MappingComponentTextBlock;
 
  const factory MappingComponents.textArea({
    required MappingComponentId id,
    required MappingComponentCoordinates coordinates,
    required String placeholder,
    required String value,
    required bool isRequired,
    required int maxLength,
  }) = MappingComponentTextArea;
}

これをriverpod_generatorで使ったとき、

part 'placed_mapping_component_text_block_notifier.g.dart';
 
@riverpod
class PlacedMappingComponentTextBlockNotifier
    extends _$PlacedMappingComponentTextBlockNotifier {
  @override
  MappingComponentTextBlock build() {
    return MappingComponentTextBlock(
      id: MappingComponentId.generate(),
      coordinates: MappingComponentCoordinates.initial(),
      value: '',
      fontSize: 16,
    );
  }
}

こんな感じになるかと思います。というかこれを書いた。
で、ここでnotifierが保持する型として、freezedで生成したsealedクラスを指定しています。

これがビルドできない。
で、なんでか調べたんですけど、

どうやら既知の問題だったらしいです。しかしながら、riverpodの開発者はこれを修正しない=rivepod側でできることがない?っぽいので、こちらで対処するほかないようです。

@riverpodでコードを生成するとき、riverpod_generatorはproviderの戻り値の型をanalyzer経由で読み取り、それをコードとして書き出します。

一方、freezedのsealedクラスの各ケース(例:MappingComponentTextBlock)は、手書きのソースコードには存在せず、freezedが裏側で生成する.freezed.dartファイルの中で初めて実体化する型でした。

この2つのジェネレータ(freezedとriverpod_generator)は同じビルドサイクルの中で並行して動くため、riverpod_generatorがproviderの戻り値の型を解決しようとしたタイミングで、freezedがまだその型を生成し切っていないことがあります。その結果、riverpod_generatorはその型をInvalidTypeとして扱ってしまい、InvalidTypeExceptionでクラッシュする、という流れですかね。

対処

対処としては、freezedejectという機能を使います。
詳細は以下の記事が詳しい

簡単に言うと、Freezedでconst facoty クラス名({}) = サブクラス名と書いて生成していたサブクラスを自分で定義することができます。

上のコードの例で書くとこんな感じ。

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:rerurate_flutter_template/domain/value_object/font_family.dart';
import 'package:rerurate_flutter_template/domain/value_object/font_size.dart';
import 'package:rerurate_flutter_template/domain/value_object/mapping_component_coordinates.dart';
import 'package:rerurate_flutter_template/domain/value_object/mapping_component_id.dart';
 
part 'mapping_components.freezed.dart';
 
@freezed
sealed class MappingComponents with _$MappingComponents {
  const MappingComponents._();
 
  const factory MappingComponents.textBlock({
    required MappingComponentId id,
    required MappingComponentCoordinates coordinates,
    required String value,
    required FontSize fontSize,
  }) = MappingComponentTextBlock;
 
  const factory MappingComponents.textArea({
    required MappingComponentId id,
    required MappingComponentCoordinates coordinates,
    required String placeholder,
    required String value,
    required bool isRequired,
    required int maxLength,
  }) = MappingComponentTextArea;
 
  MappingComponentId get id;
  MappingComponentCoordinates get coordinates;
}

ここまでは同じ、このコードの後にサブクラス定義を.dartファイルの中に含めていきます。

// 同ファイル 追加分
@freezed
sealed class MappingComponentTextBlock extends MappingComponents
    with _$MappingComponentTextBlock {
  const factory MappingComponentTextBlock({
    required MappingComponentId id,
    required MappingComponentCoordinates coordinates,
    required String value,
    required FontSize fontSize,
  }) = _MappingComponentTextBlock;
 
  const MappingComponentTextBlock._() : super._();
}
 
@freezed
sealed class MappingComponentTextArea extends MappingComponents
    with _$MappingComponentTextArea {
  const factory MappingComponentTextArea({
    required MappingComponentId id,
    required MappingComponentCoordinates coordinates,
    required String placeholder,
    required String value,
    required bool isRequired,
    required int maxLength,
  }) = _MappingComponentTextArea;
 
  const MappingComponentTextArea._() : super._();
}

実際には.freezed.dartファイルの中でこういったクラスが生成されるのですが、これを自分で定義して書いた形になります。

freezedのジェネレータは、サブクラス定義と同じクラス名のクラスを発見した場合、.freezed.dartの中で生成することをスキップし、その発見したクラスをサブクラス定義として使用する挙動をとります。実際にはejectと呼ばれる機能になります。

こうすることで、riverpodのジェネレータがきちんと型を取得することができるようになって、最初のエラーが解決し、.g.dartファイルが生成されます。

おわり

おわり!!!

正直ちょっとだるいやり方。
この問題がなかったらよかったんだけど。

ちなみに他にもいろんなジェネレータでこの問題が報告されているようです。