1

I'm trying to have a base Freezed interface which my app entity interfaces can extend so I can call the freezed functions on the interfaces. I've started the process here which seems to be working so far:

abstract class IUserRegistrationEntity<T> extends FreezedClass<T> {
  String get nickName;
  String get email;
  String get confirmEmail;
  String get password;
  String get confirmPassword;
}

abstract class FreezedClass<T> {
  T get copyWith;
  Map<String, dynamic> toJson();
}

freezed class:

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:vepo/domain/user_registration/i_user_registration_entity.dart';

part 'user_registration_entity.freezed.dart';
part 'user_registration_entity.g.dart';

@freezed
abstract class UserRegistrationEntity with _$UserRegistrationEntity {
  @Implements.fromString(
      'IUserRegistrationEntity<\$UserRegistrationEntityCopyWith<IUserRegistrationEntity>>')
  const factory UserRegistrationEntity(
      {String nickName,
      String email,
      String confirmEmail,
      String password,
      String confirmPassword}) = _IUserRegistrationEntity;

  factory UserRegistrationEntity.fromJson(Map<String, dynamic> json) =>
      _$UserRegistrationEntityFromJson(json);
}

But now I need to add the fromJson factory constructor to the interface. I think this may be what I'm looking for although I can't really tell how to implement it in my code:

 T deserialize<T extends JsonSerializable>(
    String json,
    T factory(Map<String, dynamic> data),
  ) {
    return factory(jsonDecode(json) as Map<String, dynamic>);
  }
You an then call it with:

var myValue = deserialize(jsonString, (x) => MyClass.fromJson(x));

Any help adding the fromJson to my freezed interface would be appreciated.

BeniaminoBaggins
  • 11,202
  • 41
  • 152
  • 287
  • Same problem here. did you ever find an answer to your questions? – Martin Bissegger Apr 01 '21 at 00:07
  • No, I think they just have to be added to your interfaces manually. Make all your freezed classes extend something like this, then you can type them as the base when you use them: `abstract class BaseFreezedClass { const BaseFreezedClass(); T get copyWith; Map toJson(); }`. Sorry I haven't added all of the methods to the super class. – BeniaminoBaggins Apr 01 '21 at 04:05
  • @MartinBissegger I believe my new answer is a better solution. – BeniaminoBaggins Jul 03 '21 at 02:51

2 Answers2

2

I've found a way to get the same benefits of programming to an interface or "abstraction" with freezed objects, while still getting to call those freezed functions:

@freezed
abstract class Person with _$Person {
  @With(BasicPersonMixin)
  const factory Person.basicPerson(
      {int? id, String? firstName, String? lastName}) = BasicPerson;

  @With(FancyPersonMixin)
  const factory Person.fancyPerson({String? firstName, required String extraPropMiddleName, String? lastName}) =
      FancyPerson;

  factory Person.fromJson(Map<String, dynamic> json) => _$PersonFromJson(json);

  const Person._();

  void functionThatEveryPersonShares() {
    print('I am a person');
  }

  String greet() {
    return 'override me with a mixin or abstract class';
  }
}

mixin FancyPersonMixin {

  String get extraPropMiddleName {
      return 'my default middle name is John`;
  }

  String greet() {
    return 'Salutations!';
  }

  void specialisedFunctionThatOnlyIHave() {
    print('My middle name is $extraPropMiddleName');
  }
}

mixin BasicPersonMixin {
  String greet() {
    return 'Hi.';
  }
}

Now we have 2 concrete classes: BasicPerson, and FancyPerson which are both a Person. Now we can program to Person throughout the app, and still call .copyWith and .fromJson and so on and so forth. The different types of Person can vary independently from each other by using mixins and still be used as a Person type. Works with generics etc (from docs - @With.fromString('AdministrativeArea<House>')) but I have kept the example simple for this question to most simply show the benefits. You can also make Person extend another base class.

BeniaminoBaggins
  • 11,202
  • 41
  • 152
  • 287
  • Ok this is interesting. I'm currently neck deep in this "how do you make a Freezed model interface" puzzle. So if I understand above: The Person class is sorta the interface and the mixin classes are sorta the child class exposed through the factory constructors in Person. I think? I may try this. – Locohost Sep 28 '21 at 16:12
  • Is this the only way to make a Flutter/Dart Freezed model "interface" right now? Wouldn't I want the fromJson, toJson and copyWith in the mixin though? – Locohost Sep 28 '21 at 16:15
  • @Locohost Yes, this is the only way. I have used it fairly extensively now, so let me know if you have any more questions. I move the mixins to their own file for that separation. You do end up with a large freezed class file of factories (the Person class in example). Those "fromJson", "toJson", "copyWith" are in the freezed generated code, so you don't worry about adding them to the mixin. Referring to a `Person` in code, `Person` will have those methods on it, strongly typed and working. So will `FancyPerson` and `BasicPerson`. "fromJson" has a switch case and creates the right subtype. – BeniaminoBaggins Sep 29 '21 at 04:17
  • Working on this just now. Question: What is the switch test in the fromJson? – Locohost Sep 29 '21 at 13:00
  • Oh Ok I see, Freezed created the switch. Very nice :-) – Locohost Sep 29 '21 at 13:23
  • @Locohost One gotcha with `fromJson` is that json objects (DTO's) that don't come from your front end will not have a `runTimeType` property. This will cause the switch case in `fromJson` to go to the default case which is an error. My back end DTO's all have `Discriminator` property attached by default which is the name of the class as a string. EF Core does that automatically. So I place this above my freezed class and it tells `fromJson` to use that instead of `runTimeType` . `@Freezed(unionKey: 'discriminator', unionValueCase: FreezedUnionCase.pascal)` – BeniaminoBaggins Sep 29 '21 at 17:45
  • Ok you have some strong psychic powers :-D I am here literally right now to mention json['runtimeType'] is always null. – Locohost Sep 29 '21 at 20:39
  • When you have a few mins, can you say more about: "...don't come from your front end...". I'm not following that. – Locohost Sep 29 '21 at 20:43
  • So I put the discriminator property on each Model (factory constr), and I do see the fromJson in base now is testing json['discriminator'] value like I want, but now this value is always null. – Locohost Sep 29 '21 at 21:03
  • Do I have to add the discriminator prop to each of my classes or does the generator do that for me? I've tried both (manual and auto assumption) and json['discriminator'] is always null – Locohost Sep 29 '21 at 21:16
  • Oh wait, Ok, its because I'm pulling data from Firestore and the docs are missing that prop. Sorry :-/ – Locohost Sep 29 '21 at 21:21
  • @Locohost I mean if your JSON is coming from an http request. The JSON won't have the `runTimeType` field. You may need to make sure you save the object in Firebase with `discriminator` so it doesn't get lost in translation. You can add default properties to freezed objects so you can add `@Default('FancyPerson') String? discriminator` to your subType factory to make sure the JSON that you send to Firebase has that property so you don't lose the property. I have a DTO version and a normal version. My DTO version has `discriminator` with a default value. Does yours work now? – BeniaminoBaggins Sep 29 '21 at 22:52
  • @Locohost Ah, or it might be cleaner to add the discriminator field with 'FancyPerson' directly to the FancyPersonMixin and leave it off the freezed factory since it will never change, I THINK that is possible. – BeniaminoBaggins Sep 29 '21 at 23:02
  • Yes it appears to be working now. Even my BaseRepo uses this new big base freezed model in the T. Pretty good improvement far. Only minor complaint is the huuuuge single model file and the fact that freezed models seem extra complex to me. More complex than what I'm used to :-) – Locohost Sep 30 '21 at 14:35
  • I'm very new to Dart, learning fast. Factory constructors are odd to me coming from C# and 20+ years of JS. – Locohost Sep 30 '21 at 14:39
  • @Locohost I can totally relate. Freezed in particular can be tricky as it requires quite specific syntax to work in some advanced cases. Another gotcha I just found is that yes you can add the properties that will never change like `discriminator` to the `FancyPersonMixin` and thus remove those fields from all the factories - but **make sure** they are _all getters_ and that will enable the classes/factories to all remain `const` which is better for performance. – BeniaminoBaggins Sep 30 '21 at 17:52
  • @Locohost I essentially [asked another code generator package creator to create a base interface to work with](https://github.com/artflutter/reactive_forms_generator/issues/15), in their package, and they did it. I wonder if `Freezed` would be able to have this done too, to give us a base interface that has copyWith because that would allow us to write even more abstract code than this solution. – BeniaminoBaggins Dec 30 '21 at 19:14
0

I've found another way to let you be a bit more abstract than my other answer. Say you're in a highly abstract super-class, so you don't want to work with objects as specific as Person. You want to work with "a base freezed object"; just cast your type to dynamic in brackets and go ahead and use copyWith freely. Sure, it's not typesafe, but it's a worthy option if it allows you to do something in a super-class rather than in every sub-class.

mixin LocalSaveMixin<TEntity extends LocalSaveMixin<TEntity>> on Entity {
  LocalRepository<TEntity> get $repository;
  Ref? get provider;

  TEntity $localFetch() {
    return ($repository.$localFetch() as dynamic).copyWith(provider: provider)
        as TEntity;
  }

  TEntity $localSave() {
    return $repository.$localSave(entity: this as TEntity);
  }
}
BeniaminoBaggins
  • 11,202
  • 41
  • 152
  • 287