Dart: Custom "copyWith" method with nullable properties - dart
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.
One solution is to use a function to set the value. This gives you more options.
A function that isn't provided: null
This will not change the value
A function that is provided and returns null: () => null
This will set the value to null
A function that returns the name: () => 'Gustavo'
This will set the value to 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.
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
}
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());
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.
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());
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.
Sometimes you have also implemented toMap() and fromMap() (or any other kind of "serialization") so:
class Person {
final String? name;
Person(this.name);
Person copyWith({String? name}) => Person(name ?? this.name);
Map<String,dynamic> toMap(){
return {'name':name};
}
Person fromMap(Map<String,dynamic> map){
return Person(map['name']);
}
}
void main() {
final person = Person("Gustavo");
print(person.name); // prints Gustavo
// I want the name property to be null
final personMap = person.toMap();
personMap['name']=null;
final person2 = person.fromMap(personMap);
print(person2.name); // Prints null
}
You could define type based const values and use them as default value for the copyWith parameters and then check if it the value of the parameter is equal to the const value. The problem is that you have to change the defines, if they will be needed in the future. Also it could be awkward to define constant values for some classes.
const String copyWithString = "null";
const double copyWithDouble = -9999999;
class Person {
final double? age;
final String? name;
Person({this.name, this.age});
Person copyWith({
String? name = copyWithString,
double? age = copyWithDouble,
}) {
return Person(
name: name == copyWithString ? this.name : name,
age: age == copyWithDouble ? this.age : age,
);
}
}
void main() {
final person = Person(name: 'John', age: 30);
print(person.name); // Prints John
print(person.age); // Prints 30
final person2 = person.copyWith(age: 31);
print(person2.name); // Prints John
print(person2.age); // Prints 31
final person3 = person.copyWith(name: 'Cena');
print(person3.name); // Prints Cena
print(person3.age); // Prints 31
final person4 = person.copyWith(name: null, age: null);
print(person4.name); // Prints null
print(person4.age); // Pritns null
}
here is a working dartpad
Just change your copyWith like below:
Person copyWith({String? name}) => Person(name);
Gustavo
null
Since name is declared String?, what is the point of checking again before assigning it like below?
Person copyWith({String? name}) => Person(name ?? this.name);
Related
Why it returns Instance of instead of the value?
Why the getCurrencyFromAPI function returns Intance of currency instead of the value itself. Is there some thing wrong with my model class? This is the function import 'dart:convert'; import 'package:app_bloc/data/models/currency.dart'; import 'package:http/http.dart' as http; import 'package:app_bloc/constants/api_urls.dart'; class Repository { Future<dynamic> getCurrencyFromAPI() async { final res = await http.get(Uri.parse(coinbaseURL)); if (res.statusCode == 200) { final resData = jsonDecode(res.body); final data = resData['data'] as List; List<Currency> list = []; for (var e in data) { final a = Currency.fromJson(e); list.add(a); } print(list); } else { throw Exception('Error fetching data from API'); } } } void main(List<String> args) { Repository repo = Repository(); repo.getCurrencyFromAPI(); } this is the model class class Currency { String id; String name; String minSize; Currency({required this.id, required this.name, required this.minSize}); factory Currency.fromJson(Map<String, dynamic> data) { final id = data['id'] as String; final name = data['name'] as String; final minSize = data['min_size'] as String; return Currency(id: id, name: name, minSize: minSize); } }
Your Currency class does not have a toString method. That means it inherits the default from Object which returns Instance of 'Currency'. When you print the List<Currency> it calls toString on every element to get a string representation. So, that's what you see. It is a Currency object. Try adding: String toString() => "Currency(id: $id, name: $name, minSize: $minSize)"; to you Currency class and see if it makes a difference.
Currency currencyModelFromJson(String str) => Currency.fromJson(json.decode(str)); class Currency { String id; String name; String minSize; Currency({required this.id, required this.name, required this.minSize}); factory Currency.fromJson(Map<String, dynamic> data) { final id = data['id'] as String; final name = data['name'] as String; final minSize = data['min_size'] as String; return Currency(id: id, name: name, minSize: minSize); } } Then do this : class Repository { Future<dynamic> getCurrencyFromAPI() async { final res = await http.get(Uri.parse(coinbaseURL)); if (res.statusCode == 200) { final resData = jsonDecode(res.body); final data = resData['data'] as List; List<Currency> list = []; for (var e in data) { final a = currencyModelFromJson(e); // change here list.add(a); } print(list); } else { throw Exception('Error fetching data from API'); } } }
How can I differentiate between an empty optional named parameter and an explicitly pass null value? [duplicate]
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.
One solution is to use a function to set the value. This gives you more options. A function that isn't provided: null This will not change the value A function that is provided and returns null: () => null This will set the value to null A function that returns the name: () => 'Gustavo' This will set the value to 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.
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 }
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());
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.
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());
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.
Sometimes you have also implemented toMap() and fromMap() (or any other kind of "serialization") so: class Person { final String? name; Person(this.name); Person copyWith({String? name}) => Person(name ?? this.name); Map<String,dynamic> toMap(){ return {'name':name}; } Person fromMap(Map<String,dynamic> map){ return Person(map['name']); } } void main() { final person = Person("Gustavo"); print(person.name); // prints Gustavo // I want the name property to be null final personMap = person.toMap(); personMap['name']=null; final person2 = person.fromMap(personMap); print(person2.name); // Prints null }
You could define type based const values and use them as default value for the copyWith parameters and then check if it the value of the parameter is equal to the const value. The problem is that you have to change the defines, if they will be needed in the future. Also it could be awkward to define constant values for some classes. const String copyWithString = "null"; const double copyWithDouble = -9999999; class Person { final double? age; final String? name; Person({this.name, this.age}); Person copyWith({ String? name = copyWithString, double? age = copyWithDouble, }) { return Person( name: name == copyWithString ? this.name : name, age: age == copyWithDouble ? this.age : age, ); } } void main() { final person = Person(name: 'John', age: 30); print(person.name); // Prints John print(person.age); // Prints 30 final person2 = person.copyWith(age: 31); print(person2.name); // Prints John print(person2.age); // Prints 31 final person3 = person.copyWith(name: 'Cena'); print(person3.name); // Prints Cena print(person3.age); // Prints 31 final person4 = person.copyWith(name: null, age: null); print(person4.name); // Prints null print(person4.age); // Pritns null } here is a working dartpad
Just change your copyWith like below: Person copyWith({String? name}) => Person(name); Gustavo null Since name is declared String?, what is the point of checking again before assigning it like below? Person copyWith({String? name}) => Person(name ?? this.name);
How to pass null in a method?
class Foo { final int? i; Foo({this.i}); Foo copyWith({int? x}) { return Foo(i: x ?? i); } } void main() { final foo = Foo(i: 0); foo.copyWith(x: null); print(foo.i); // prints `0` but should print `null`. } How can I actually pass null value to the method? In earlier Dart version copyWith() and copyWith(x: null) were two different things. Note: I'm not looking for workarounds like making a new variable, like isNull and then deciding whether to pass null or not based on its value.
With simple copyWithwhit Dart null-safety you can't override value by null because if id is null return this.id. You need to override the value by null but not return with another value. It can solve in a few ways but I will give you the best example. void main() { final user = User(name: 'Dave', id: 110); User copy = user.copyWith(id: null); print(copy.toString()); // prints User(name: Dave, id: null). } class User { User({required this.name, this.id}); final String name; final int? id; UserCopyWith get copyWith => _UserCopyWith(this); #override String toString() => 'User(name: $name, id: $id)'; } abstract class UserCopyWith { User call({ String name, int? id, }); } class _UserCopyWith implements UserCopyWith { _UserCopyWith(this.value); final User value; static const _undefined = Object(); #override User call({ Object name = _undefined, Object? id = _undefined, }) { return User( name: name == _undefined ? value.name : name as String, id: id == _undefined ? value.id : id as int?, ); } }
How to create null safe, default value constructor with Syntactic sugar
How do I create a null safe constructor with Syntactic sugar that would set a default value if the provided value is null? class Person { Person({ required this.name, //Idealy, adding (?? "friend") instead of "required" should've worked but doesn't. required this.age, }); String name; int age; greet() { print("Hello $name"); } } So, I actually want something like this, class Person { Person({ this.name ?? "friend", this.age ?? 0, }); String name; int age; greet() { print("Hello $name"); } } But, as you know this is not valid in dart. So, how actually, should I achieve this?
class Person { Person({ String? name, int? age, }) : this.name = name ?? "friend", this.age = age ?? 0; String name; int age; void greet() { print("Hello $name"); } }
Constructor Optional Params for selecting my proposal select this as an answer (converted from comment with permission)
You can also use default values for your optional parameters: class Person { Person({ this.name = "friend", this.age = 0, }); String name; int age; greet() { print("Hello $name"); } } The parameter is not required, and if you don't pass it, it gets the default value. If you do pass an argument, it must be non-null.
how should I use assert in Dart?
I saw exmaple code something like: class ModelBinding extends StatefulWidget { ModelBinding({ Key key, this.initialModel = const GalleryOptions(), this.child, }) : assert(initialModel != null), super(key: key); ... so I wrote something: class Person { String firstName; Person({name}){ print(name); } } class Employee extends Person { Employee(String name) : assert(false), super(name: name); } main() { var emp = new Employee('Jason'); } No matter if it is assert(false) or assert(true), the result is same. So what is the meaning of assert?
assert is used for debugging and it simply means the condition should be true to proceed. Let me explain: class MyClass { final int age; MyClass({this.age}); void someMethod() { // using `age` here } } You might face issues in someMethod if age passed is null, so to make sure it isn't null, you use assert like: class MyClass { final int age; MyClass({this.age}) : assert(age != null, "Make sure age isn't null"); void someMethod() { // using `age` here } }