xsahil03x / super_enum

Create super-powered dart enums similar to sealed classes in Kotlin
https://pub.dev/packages/super_enum
MIT License
116 stars 13 forks source link

Unhandled Exception when using Generics #22

Closed chimon2000 closed 4 years ago

chimon2000 commented 4 years ago

When using generics, I am getting a type casting exceptions given the following example

@superEnum
enum _ModelState {
  @generic
  @Data(fields: [DataField('data', Generic)])
  Success,

  @object
  Error,

  @object
  Loading,

  @object
  Empty,
}

//Somewhere else in code
Stream<ModelState<int>> getSomething() {
  return Stream.fromFuture(Future.value(1))
      .publishReplay(maxSize: 1)
      .refCount()
      .map((data) => ModelState.loading());
}

The error:

Unhandled Exception: type 'Loading<dynamic>' is not a subtype of type 'Loading<int>'

Environment:

Flutter 1.12.13+hotfix.6
Framework • revision 18cd7a3601 (7 days ago) • 2019-12-11 06:35:39 -0800
Engine • revision 2994f7e1e6
Tools • Dart 2.7.0

Dependencies:

dependencies:
  super_enum: ^0.2.0

dev_dependencies:
  super_enum_generator: ^0.2.0+1
  build_runner: ^1.7.1
chimon2000 commented 4 years ago

Seems like generics could be more easily handled using classes rather than enums. What's the value added by using enums?

astralstriker commented 4 years ago

You have missed specifying the type with the factory constructor.

It should be

@superEnum
enum _ModelState {
  @generic
  @Data(fields: [DataField('data', Generic)])
  Success,

  @object
  Error,

  @object
  Loading,

  @object
  Empty,
}

//Somewhere else in code
Stream<ModelState<int>> getSomething() {
  return Stream.fromFuture(Future.value(1))
      .publishReplay(maxSize: 1)
      .refCount()
      .map((data) => ModelState<int>.loading());
}

Dart is unable to automatically infer template types.

As for the values added by enums are brevity of the code, simplicity and cohesion with the idea of sealed classes being superlative enums. If dart had support for nested classes we could have had used classes but as per your example of sealed_unions, you have to rely on member methods to generate corresponding classes and factories. Neither it's readable nor does it make much sense.

chimon2000 commented 4 years ago

@astralstriker makes sense, thanks for pointing out that fix! That appeared to solve my initial issue, however I encounter the same error when using startWith

Stream<ModelState<int>> getSomething() {
  return Stream.fromFuture(Future.value(1))
      .publishReplay(maxSize: 1)
      .refCount()
      .map((data) => ModelState<int>.success(data: data))
      .startWith(ModelState<int>.loading());

It shouldn't matter, but I am using a more complex type and seeing the same error when I consume the observable.

    return Stream.fromFuture(Future.value())
        .publishReplay(maxSize: 1)
        .refCount()
        .map((data) => ModelState<VehicleTypeResults>.loading());

This appears to result in the same error. The only call that has worked correctly for me is success.

chimon2000 commented 4 years ago

I believe the issue is in how non-generic classes are created with singletons. Notice the following screenshot that shows the _instance is dynamic.

image

If I swap the generated code for Loading with similar code as Success the issue is resolved.

class Loading<T> extends ModelState<T> {
  const Loading._() : super(_ModelState.Loading);

  factory Loading() {
    _instance ??= Loading._();
    return _instance;
  }

  static Loading _instance;
}

//Becomes 
class Loading<T> extends ModelState<T> {
  const Loading() : super(_ModelState.Loading);
}

From what I understand, static members also cannot be referenced with generic types. See https://github.com/dart-lang/language/issues/359