Search code examples
flutterdartfreezedjson-serializable

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?


Solution

  • 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.