'Dart: Custom "copyWith" method with nullable properties
I'm trying to create a "copyWith" method for my class, and it works with most scenarios.
The problem is when I try to set a nullable property to null, because my function cannot recognize whether it's intentional or not.
Ex.:
class Person {
final String? name;
Person(this.name);
Person copyWith({String? name}) => Person(name ?? this.name);
}
void main() {
final person = Person("Gustavo");
print(person.name); // prints Gustavo
// I want the name property to be nul
final person2 = person.copyWith(name: null);
print(person2.name); // Prints Gustavo
}
Does anyone knows some workaround for this situation? This is really bothering me and I don't know how to avoid this situation.
Solution 1:[1]
Inspired by @jamesdlin answer:
All you need to do is provide a wrapper around. Take this example in consideration:
class Person {
final String? name;
Person(this.name);
Person copyWith({Wrapped<String?>? name}) =>
Person(name != null ? name.value : this.name);
}
// This is all you need:
class Wrapped<T> {
final T value;
const Wrapped.value(this.value);
}
void main() {
final person = Person('John');
print(person.name); // Prints John
final person2 = person.copyWith();
print(person2.name); // Prints John
final person3 = person.copyWith(name: Wrapped.value('Cena'));
print(person3.name); // Prints Cena
final person4 = person.copyWith(name: Wrapped.value(null));
print(person4.name); // Prints null
}
Solution 2:[2]
Another solution is to use a function to set the value. This way, you can determine between a function that isn't provided (null
), a function that is provided and returns () => null
, and a function that returns the name () => 'Gustavo'
)
class Person {
final String? name;
Person(this.name);
Person copyWith({String? Function()? name}) =>
Person(name != null ? name() : this.name);
}
void main() {
final person = Person('Gustavo');
print(person.name); // prints Gustavo
// I want the name property to be nul
final person2 = person.copyWith(name: () => null);
print(person2.name); // Prints null
final person3 = person.copyWith(name: () => 'new name');
print(person3.name); // Prints new name
final person4 = person.copyWith();
print(person4.name); // Prints Gustavo
}
It makes setting the name slightly more cumbersome, but on the bright side the compiler will tell you that you've provided the wrong type if you try to pass a string directly, so you will be reminded to add the () =>
to it.
Solution 3:[3]
Person.name
is declared to be non-nullable, so it is impossible for copyWith
to assign a null value to it. If you want Person.name
to be nullable, you should ask yourself if you really want a distinction between null
and an empty string. Usually you don't.
If you actually do want to allow both null
and empty strings, then you either will need to use some other sentinel value:
class Person {
static const _invalid_name = '_invalid_name_';
final String? name;
Person(this.name);
Person copyWith({String? name = _invalid_name}) =>
Person(name != _invalid_name ? name : this.name);
}
or you will need to wrap it in another class, e.g.:
class Optional<T> {
final bool isValid;
final T? _value;
// Cast away nullability if T is non-nullable.
T get value => _value as T;
const Optional()
: isValid = false,
_value = null;
const Optional.value(this._value) : isValid = true;
}
class Person {
final String? name;
Person(this.name);
Person copyWith({Optional<String?> name = const Optional()}) =>
Person(name.isValid ? name.value : this.name);
}
void main() {
final person = Person("Gustavo");
print(person.name);
final person2 = person.copyWith(name: Optional.value(null));
print(person2.name);
}
There are existing packages that implement Optional
-like classes that probably can help you.
Solution 4:[4]
I'm using the Optional package to work around the problem, so the code looks something like this:
final TZDateTime dateTime;
final double value;
final Duration? duration;
...
DataPoint _copyWith({
TZDateTime? dateTime,
double? value,
Optional<Duration?>? duration}) {
return DataPoint(
dateTime ?? this.dateTime,
value ?? this.value,
duration: duration != null ?
duration.orElseNull :
this.duration,
);
}
In this example, duration is a nullable field, and the copyWith pattern works as normal. The only thing you have to do differently is if you are setting duration
, wrap it in Optional like this:
Duration? newDuration = Duration(minutes: 60);
_copyWith(duration: Optional.ofNullable(newDuration));
Or if you want to set duration to null:
_copyWith(duration: Optional.empty());
Solution 5:[5]
At the expense of making the implementation of copyWith
twice as big, you can actually use flags to allow null
-ing the fields without any use of a "default empty object" or Options class:
class Person {
final String? name;
final int? age;
Person(this.name, this.age);
Person copyWith({
String? name,
bool noName = false,
int? age,
bool noAge = false,
// ...
}) =>
Person(
name ?? (noName ? null : this.name),
age ?? (noAge ? null : this.age),
// ...
);
}
void main() {
final person = Person('Gustavo', 42);
print(person.name); // prints Gustavo
print(person.age); // Prints 42
final person2 = person.copyWith(noName: true, age: 84);
print(person2.name); // Prints null
print(person2.age); // Prints 84
final person3 = person2.copyWith(age: 21);
print(person3.name); // Prints null
print(person3.age); // Prints 21
final person4 = person3.copyWith(name: 'Bob', noAge: true);
print(person4.name); // Prints Bob
print(person4.age); // Prints null
runApp(MyApp());
}
It does have the pointless case of:
final person = otherPerson.copyWith(name: 'John', noName: true);
but you can make asserts for that if you really want to disallow it I suppose.
Solution 6:[6]
There are multiple options:
1. ValueGetter
class B {
const B();
}
class A {
const A({
this.nonNullable = const B(),
this.nullable,
});
final B nonNullable;
final B? nullable;
A copyWith({
B? nonNullable,
ValueGetter<B?>? nullable,
}) {
return A(
nonNullable: nonNullable ?? this.nonNullable,
nullable: nullable != null ? nullable() : this.nullable,
);
}
}
const A().copyWith(nullable: () => null);
const A().copyWith(nullable: () => const B());
2. Optional from Quiver package
class B {
const B();
}
class A {
const A({
this.nonNullable = const B(),
this.nullable,
});
final B nonNullable;
final B? nullable;
A copyWith({
B? nonNullable,
Optional<B>? nullable,
}) {
return A(
nonNullable: nonNullable ?? this.nonNullable,
nullable: nullable != null ? nullable.value : this.nullable,
);
}
}
const A().copyWith(nullable: const Optional.fromNullable(null));
const A().copyWith(nullable: const Optional.fromNullable(B()));
3. copyWith as field
class _Undefined {}
class B {
const B();
}
class A {
A({
this.nonNullable = const B(),
this.nullable,
});
final B nonNullable;
final B? nullable;
// const constructor no more avaible
late A Function({
B? nonNullable,
B? nullable,
}) copyWith = _copyWith;
A _copyWith({
B? nonNullable,
Object? nullable = _Undefined,
}) {
return A(
nonNullable: nonNullable ?? this.nonNullable,
nullable: nullable == _Undefined ? this.nullable : nullable as B?,
);
}
}
A().copyWith(nullable: null);
A().copyWith(nullable: const B());
4. copyWith redirected constructor
class _Undefined {}
class B {
const B();
}
abstract class A {
const factory A({
B nonNullable,
B? nullable,
}) = _A;
const A._({
required this.nonNullable,
this.nullable,
});
final B nonNullable;
final B? nullable;
A copyWith({B? nonNullable, B? nullable});
}
class _A extends A {
const _A({
B nonNullable = const B(),
B? nullable,
}) : super._(nonNullable: nonNullable, nullable: nullable);
@override
A copyWith({B? nonNullable, Object? nullable = _Undefined}) {
return _A(
nonNullable: nonNullable ?? this.nonNullable,
nullable: nullable == _Undefined ? this.nullable : nullable as B?,
);
}
}
const A().copyWith(nullable: null);
const A().copyWith(nullable: const B());
5. copyWith redirected constructor 2
class _Undefined {}
class B {
const B();
}
abstract class A {
const factory A({
B nonNullable,
B? nullable,
}) = _A;
const A._();
B get nonNullable;
B? get nullable;
A copyWith({B? nonNullable, B? nullable});
}
class _A extends A {
const _A({
this.nonNullable = const B(),
this.nullable,
}) : super._();
@override
final B nonNullable;
@override
final B? nullable;
@override
A copyWith({B? nonNullable, Object? nullable = _Undefined}) {
return _A(
nonNullable: nonNullable ?? this.nonNullable,
nullable: nullable == _Undefined ? this.nullable : nullable as B?,
);
}
}
const A().copyWith(nullable: null);
const A().copyWith(nullable: const B());
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 | Daniel Arndt |
Solution 3 | |
Solution 4 | James Allen |
Solution 5 | Mat |
Solution 6 | maRci002 |