3

I can't seem to understand how to make abstract classes and their subclasses work properly with Freezed. Specifically, I need to define an abstract super-class with some to-override getters; the subclasses will need to override the getters, while exploiting freezed to generate their boilerplate code.

I do understand what the documentation suggests, but I do need more.

I created a repo to show off what I need, in case you want to cut to the chase. All I need are these tests to pass and work as intended.

I need a clean, readable and maintainable way to do this.

Scenario

In the repo you'll find a small reproduction case:

  • A Base abstract class;
  • A class A, which extends the Base class (I wanted to simplify things, but I do have more subclasses in my real use case, say X, Y, Z, etc.);
  • A needs to be implemented with Freezed, with JSON serialization / deserialization;

Recap: Base is there to have write common ground between A, X, Y and Z; as mentioned above, Freezed is a must-have for these subclasses.

Goal

My goal is to exploit composition.

Let a class B have a Base get myValue getter, with Freezed. Unsurprisingly, I want to interact with this value by accessing its copyWith method (or others, like toJson), but this gets complicated quite fast (see Problem 1).

Again, read the tests for the desired outcome.

Problem 1

Implementing what I've described above, while it makes sense, is no easy task (to my understanding).

For example, the following:

abstract class Base {
  const Base();

  Function copyWith();
  Map<String, dynamic> toJson();

  String get id;
}

won't work because the subclasses will feel ambiguity onto which superclass method to use (error here): should it use the one generated by @freezed, or the abstract one? That's a compile-time error.

I have no clue how to properly write a contract while exploiting Freezed.

Problem 2

At first, build_runner rightfully complains as it can't generate the fromJson method onto B because Base has no @JsonSerializable method.

This would imply implementing a converter manually: I did so, but this gets old quickly. This implies to re-write the converter for each new subclass created and raise an exception for a subclass that isn't handled, yet (that's a strong code smell).

Such atrocity is found in this file.

How can achieve this in a clean way?

venir
  • 1,809
  • 7
  • 19

2 Answers2

1

After two days of work and research, I managed to get out of this mess.

  1. I had to drop freezed for now (I'll just keep it for the union type system);
  2. Implementing this by hand is out of the question, obviously;
  3. I used this package: dart_mappable, that does exactly what I've asked above.

I'll post here a quick pseudocode implementation achievable with dart_mappable:

@MappableClass()
abstract class MyBase with MyBaseMappable {
  const MyBase(this.id);
  final String id;
}

@MappableClass()
class A extends MyBase with AMappable {
  const A(super.id);
}

@MappableClass()
class B with BMappable {
  const B(this.value);
  final MyBase value;
}


void main() {
  const a = A('hello');
  const b = B(a);
  print(b.value.copyWith(id: "lol"));  // Yes!
}

This requires v2 of dart_mappable to work, which is currently in a pre-release state.

venir
  • 1,809
  • 7
  • 19
0

Your problem likely stems from Base defining an empty copyWith

Freezed doesn't implement copyWith as a method, but as a getter that returns a function (or callable object). That's necessary for copyWith(foo: null) support, but is incompatible with your interface.

Extending Base could also be an issue. Freezed doesn't support inheritance too well because of language limitations. You're probably better off with implementing Base or making it a mixin.

All in all, you could do:

mixin Base {
  int get id;

  Map<String, dynamic> toJson();
}

@freezed
class A with _$A, Base {
  const A._();

  factory A({
    required int id;
  }) = _A;
  
  factory A.fromJson(Map<String, dynamic> json) => _$AFromJson(json);

  @override
  Map<String, dynamic> toJson() => _$AToJson(this);
}
Rémi Rousselet
  • 256,336
  • 79
  • 519
  • 432
  • Thanks, Remi!! At the end of the day, all I need is a way to access Freezed' generated methods from a common interface, plus a few common getters that every subclass needs to implement. I'll try this Mixin and see if I can get away with it. – venir Oct 27 '22 at 09:21
  • 1
    No, unluckily this isn't working. Please check my repo again (I've just pushed these suggestions) and see that: (1) I still have to implement a `BaseConverter`, which is another smell, or else `B` won't generate its code; and (2) something breaks up in the generation of class `A` as `a.freezed.dart` contains this error: `The method 'copyWith' isn't defined for the type ''`. Maybe you wanted to write `class A with _$A, Base` ? – venir Oct 27 '22 at 09:41
  • Well `Base` does not have a `fromJson`, so you can't magically decode it. If you don't want the JsonConverter, feel free to add a `fromJson` to `Base` – Rémi Rousselet Oct 27 '22 at 09:49
  • 1
    Furthermore, not having `copyWith` as a getter on the mixin is still an open problem for me, I can't get out from that. – venir Oct 27 '22 at 10:03
  • Hi again and thanks again for the help! I realize that `Base` hasn't a `fromJson`, my goal was to trigger its subclasses, but... that's impossible to make happen! That's a flaw in my reasoning. `B.fromJson` has no way to know if it should trigger `A.fromJson` or any other `X.fromJson` that subclass `Base`. So a custom `BaseConverter` must be implemented, there's no escaping that. After even more reasoning, I came to realize that this problem just isn't solvable with Freezed and that I must implement everything by hand. I'll help myself with Freezed in other contexts. – venir Oct 27 '22 at 10:37
  • Specifically, (1) I can't solve the `copyWith` abstract method problem and (2) solving the aforementioned problem by introducing a `key` property messes up the solution you've found (`toJson` isn't really customizable because the converter will cass that method onto a `_$_A` type, see repo). If Dart had dataclasses I'd just make `Base` an abstract dataclass and that would be it. Sad. – venir Oct 27 '22 at 10:44
  • Anyways, thanks for your help, I can't mark your answer as accepted as I can't tell my problems are exactly "solved". I'll wait for metaprogramming, then. – venir Oct 27 '22 at 10:48