Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Writing abstract classes while using Freezed

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?

like image 301
venir Avatar asked Oct 19 '25 03:10

venir


2 Answers

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.

like image 160
venir Avatar answered Oct 20 '25 18:10

venir


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);
}
like image 42
Rémi Rousselet Avatar answered Oct 20 '25 18:10

Rémi Rousselet



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!