data_classes icon indicating copy to clipboard operation
data_classes copied to clipboard

The future of this package

Open MarcelGarus opened this issue 6 years ago • 8 comments

For a new version of the package, I consider completely overhauling the API of this package.

Fundamental problems of this package

  • The mutable class is often not needed, people often only care about the actual immutable class.
  • The class that's actually used by all the other code is the one that the developer has the least influence over, causing friction when trying to customize it (see #2 for adding annotations to the generated class).
  • Especially with the last proposal in #3 this package becomes more and more similar to the built_value package, not offering any unique benefits.

So, what's the future of this package? The Dart team actively works on bringing static extension methods and non-nullability by default (NNBD) to Dart. That would allow this package to be even more lightweight and enable a workflow like the following:

Mark your class with an @DataClass() annotation:

@DataClass()
class User {
  final String firstName;
  final String lastName;
  final String? photoUrl;

  const User({
    this.firstName,
    this.lastName,
    this.photoUrl,
  });
}

Because of NNBD the constructor is equivalent to the current one; @required annotations and non-null assertions are no longer needed. The package could then generate the copy, ==, hashCode and toString methods as static extension methods on the User type.

Benefits

  • You have full control of the class actually being used by other parts of your code, so you can easily add annotations or doc comments as well as add custom methods right inside the class.
  • The mutable version of the class is less present as it's only used in the copy method, causing less confusion like #1.
  • You could easily customize which methods get generated by implementing them yourself -- the package will only generate methods that are not implemented yet.

Downsides

  • You need to add a constructor manually.

I'm welcoming any opinions on whether this is a worthy tradeoff.

MarcelGarus avatar Sep 12 '19 16:09 MarcelGarus

I like the approach taken by https://pub.dev/packages/sum_types. You could then do the following:

@DataClass
mixin _User {
  String get firstName;
  String get lastName;
  @nullable String get photoUrl;
}

The generated code would then be:

class User with _User {
  const User({
    @required this.firstName,
    @required this.lastName,
    this.photoUrl,
  });
}

When NNBD finally lands one could just replace the @nullable annotations with the new syntax and everything would continue to work.

tvh avatar Sep 26 '19 14:09 tvh

Interesting approach! But a possible problem I see with this is that it doesn't work well with generators of other packages, for example hive and json_serializable, because mixins are treated as abstract class and thus can't be instantiated by the generated code.

MarcelGarus avatar Sep 27 '19 06:09 MarcelGarus

True, that wouldn't be covered.

If only constructors of extended classes would just call the super class with the same args. It could be so easy:

class User extends _UserBase {
  String get firstName;
  String get lastName;
  @nullable String get photoUrl;
}

abstract class _UserBase {
  String firstName;
  String lastName;
  String photoUrl;

  _UserBase({
    this.firstName,
    this.lastName,
    this.photoUrl,
  });
}

This does compile, but completely forgets the parameters.

tvh avatar Sep 27 '19 07:09 tvh

We implemented the mixin idea of @tvh in https://github.com/factisresearch/sum_data_types The code in that repo is based on data_classes but the interface is not compatible with data_classes.

skogsbaer avatar Oct 11 '19 06:10 skogsbaer

Btw: was it intentionally that you use the 4-clause BSD license for data_classes_generator, but BSD 3-clause license for data_classes? It would be easier for us if everything was under BSD 3. Is it possible that you also license the generator part under BSD 3?

skogsbaer avatar Oct 11 '19 09:10 skogsbaer

Oh sorry, that was not intentional. To be honest, I didn't pay too much attention to the license clauses. Now I changed both to BSD 3.

MarcelGarus avatar Oct 11 '19 19:10 MarcelGarus

The package could then generate the copy, ==, hashCode and toString methods as static extension methods on the User type.

Oh nooooo, seems like that's not possible. Extensions can't declare members with the same name as a member declared by Object.

MarcelGarus avatar Oct 29 '19 13:10 MarcelGarus

Okay, I re-evaluated the current ecosystem and it seems like there are really cool alternatives in the data class code generation market. There's, of course, built_value by Google, freezed which has some really cool factory constructor syntax to declare fields and others, like hive are thinking about requiring data classes in the future. The freezed package, in particular, does many things right and address the same market like this one. All in all, it's more elegant than this one and makes some bold design decisions (like making the copyWith method not type-safe in order to support nullable fields) that make it more intuitive to use.

So, I've been thinking a bit about the future of this package and I believe the best thing to do is to try to get as close to the vision of this issue as possible: support developers writing their own data classes. There's not much tooling in this regard in the existing ecosystem, so I believe this will be a valuable addition to the Dart community rather than another competing framework.

Here's what this package could offer:

  • Implementations for ==, hashCode and toString that developers still have to incorporate into their code manually.
  • A copyWith extension method similar to freezed's one with no type-safety in favor of nullable fields.
  • Extension has getters for all fields.

Using this package would look like this:

@DataClass()
class Fruit {
  User({
    @required this.name,
    @required this.color,
    this.amount = 1,
  });

  final String name;
  final Color color;
  final int amount;

  operator ==(Object other) => _$equals(other);
  int get hashCode => _$hashCode;
  String toString() => _$toString();
}

void main() {
  var apple = Fruit(name: 'Apple', color: Colors.red);
  print(apple.toString());
  var badApple = apple.copyWith(color: Colors.brown);
  badApple.hasNoName; // false
}

MarcelGarus avatar Apr 01 '20 19:04 MarcelGarus