How to use generics with freezed sealed union objects?

1.8k Views Asked by At

I have a Flutter class that uses Freezed to create a sealed union that represents either data or an error:

@freezed
class DataOrError<T, E> with _$DataOrError {
  const factory DataOrError.loading() = Loading;

  const factory DataOrError.data(T data) = DataOrE<T, E>;

  const factory DataOrError.error(E error) = DOrError<T, E>;

  static DataOrError<T, E> fromEither<T, E>(Either<E, T> val) {
    final result = val.fold(
        (l) => DataOrError<T, E>.error(l), (r) => DataOrError<T, E>.data(r));
    return result;
  }
}

I use riverpod so I have a riverpod StateNotifier that looks like:

class RolesNotifier
    extends StateNotifier<DataOrError<List<Role>, RoleFailure>> {
  final Ref _ref;
  StreamSubscription? sub;

  RolesNotifier(Ref ref)
      : _ref = ref,
        super(const DataOrError.loading());

  /// Will run the fetch
  void fetch() {
        // fetch roles
        state = const DataOrError.loading();
        sub = _ref.read(firebaseRoleService).getRoles().listen((event) {
          state = DataOrError.fromEither<List<Role>, RoleFailure>(event);
        });
  }

// ... this class has been shortened for simplicity.
}

final rolesProvider = StateNotifierProvider.autoDispose<RolesNotifier,
    DataOrError<List<Role>, RoleFailure>>((ref) {
  return RolesNotifier(ref);
});

When I consume this provider; however, the types for DataOrError are gone:

ref
  .read(rolesProvider)
  .when(loading: (){}, data: (d) {
  // d is dynamic type not List<Role>
        
  }, error: (e){});

For some reason both d and e are dynamic types and not List<Role> & RoleFailure respectively. Everything appears to be typed correctly so why is this not working? I'm not sure if the error is with Freezed or Riverpod. I would like to avoid type casting (i.e. d as List<Role>) because that defeats the purpose of the generics.

2

There are 2 best solutions below

8
On BEST ANSWER

The mixin has to be defined with the same generic types Like

class DataOrError<T, E> with _$DataOrError<T,E>

Although if you did not explicitly define the mixin generics ,the runner will mimic the original class types but in that case there will be no relation between them. in our case it will be a different set of T and E that is why it will be dynamic , bottom line is that you have to tell the mixin that it is indeed the same set of types.

enter image description here

1
On

Your code seems to be setup correctly, my intuition is that the types are not being inferred because you are using ref.read. This may be intentional in riverpod for some reason.

Finally, using ref.read inside the build method is an anti-pattern and your UI will not change whenever the state notifier updates.

Replace ref.read(rolesProvider) with:

ref.watch(rolesProvider).when(
  loading: (){},
  data: (d) {}, // Types should be inferred correctly now
  error: (e){},
);