Skip to content
This repository has been archived by the owner on Apr 20, 2020. It is now read-only.

copyWith probably handles nulls incorrectly #3

Closed
ildarsharafutdinov opened this issue Sep 12, 2019 · 9 comments
Closed

copyWith probably handles nulls incorrectly #3

ildarsharafutdinov opened this issue Sep 12, 2019 · 9 comments

Comments

@ildarsharafutdinov
Copy link

hi,
I'm looking into your example and I guess nulls do not work properly(I didn't run it though).

  var me = const User(firstName: 'Marcel', lastName: 'Garus', photoUrl: 'http://example.com');
  var mySister = me.copyWith(firstName: 'Yvonne', photoUrl: null);

photoUrl is updated as follows photoUrl: photoUrl ?? this.photoUrl, which leads to mySister.photoUrl == 'http://example.com' instead of null.

@MarcelGarus
Copy link
Owner

MarcelGarus commented Sep 12, 2019

That is indeed true, because if a parameter to the method is null, the method doesn't know if it has been explicitly or implicitly set to null. I thought about this before but found no easy way to fix that.
Here are more complicated ways I considered:

Use mutable for copying
On the client code side you can of course convert it to a mutable, the set the fields, then re-convert it to the immutable. Doesn't change the fact that the copyWith method behaves unintuitive in some cases.

Use wrapper function
Change the signature to expect a function for nullable values (just like Iterable's firstWhere(..., orElse: () => otherValueWhichMayBeNull)).
However that leads to code that's a little bit uglier:

mySister = me.copyWith(
  firstName: 'Yvonne',
  photoUrl: () => null,
)

Default values
Third solution, add default values just like dart:ui's hashValues() function, which is defined as

const _default = Object();
int hashValues(Object arg1, [Object arg2 = _default, ...])

In the code you can check if _default == _arg2, so you can see if the user explicitly passed in null. Something like this would probably be the most elegant solution, but we'd need "sample objects" of all the Field's types as default parameter values and that's at least as far as I know impossible to achieve.

If anyone has any other suggestions on how to do this elegantly, you're welcome to share your thoughts.

@ildarsharafutdinov
Copy link
Author

I guess there is also "use mutable in copyWith" option which is similar to builders in BuiltValue:

mySister = me.copyWith((mutableCopy) {
  mutableCopy.firstName = 'Yvonne';
  mutableCopy.photoUrl = null;
});

@MarcelGarus
Copy link
Owner

Oh true, that would basically be the first option with some syntactic sugar on top.
If you want to implement that and file a PR, that would be great.

@MarcelGarus
Copy link
Owner

Thought about this a bit more.
If you have a simple widget with only non-nullable fields, using a copyWith method with a builder would be unnecessary complex.
That's why I'd rather opt for a copyWith method, where you can change all the non-nullable fields as well as copyWithBuilder method that takes a builder.
Any opinions on that?

@ildarsharafutdinov
Copy link
Author

What params will copyWith contain in case there are nullable fields in the class?
If it continues to accept nullables it will be error prone. If code-gen strips out nullables, it'll be weird.

So I'm in favor of keeping things simple with a single interface for mutations.

@MarcelGarus
Copy link
Owner

You're right.

What about we always have a builder method for mutations but if your class has only non-nullable fields, you can optionally explicitly ask for the copyWith method in the annotation, something like @DataClass(generateCopyWith=true).

And if the class contains nullable fields, that will be an error during code generation.

@ildarsharafutdinov
Copy link
Author

I'm still in favor of copyWith((mutable) => ...) :)

E.g. requirements change and a field becomes nullable. A developer will need to remove generateCopyWith=true and then modify all fancy calls of copyWith.
Whereas copyWith((mutable) => ...) won't require changes in existing code.

@MarcelGarus
Copy link
Owner

Hmm that is indeed true...
Okay, I'll implement it that way.

@MarcelGarus
Copy link
Owner

Done. The newest version 2.0.0, already published on pub, generates a copy method allowing usage like this:

var freshApple = const Fruit(type: 'apple', color: 'green');
var someApple = freshApple.copy((fruit) => fruit..color = null);
var kiwi = someApple.copy((fruit) => fruit
  ..type = 'Kiwi'
  ..color = 'brown');

The definition looks like this:

/// Copies this [Fruit] with some changed attributes.
Fruit copy(void Function(MutableFruit mutable) changeAttributes) {
  assert(
      changeAttributes != null,
      "You called Fruit.copy, but didn't provide a function for changing "
      "the attributes.\n"
      "If you just want an unchanged copy: You don't need one, just use "
      "the original.");
  var mutable = this.toMutable();
  changeAttributes(mutable);
  return Fruit.fromMutable(mutable);
}

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants