I have written this post before to make our own class comparable. It’s tedious to override the necessary methods each time when we create a new class. Equatable
package helps to override ==
operator and hashCode
but we still need to implement other methods when necessary.
However, I found another package called Freezed
. It overrides the following methods automatically.
- copyWith
- toJson
- toString
- operator ==
- hashCode
I try to use it in this post. The Flutter and Dart version is the following.
$ flutter doctor -v
[✓] Flutter (Channel stable, 3.7.9, on Linux Mint 21 5.15.0-69-generic, locale en_US.UTF-8)
• Flutter version 3.7.9 on channel stable at /home/yuto/root/development/libraries/flutter
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision 62bd79521d (5 days ago), 2023-03-30 10:59:36 -0700
• Engine revision ec975089ac
• Dart version 2.19.6
• DevTools version 2.20.1
Preparation to use Freezed
Some packages are required to be added first.
flutter pub add freezed_annotation
flutter pub add --dev build_runner
flutter pub add --dev freezed
# if using freezed to generate fromJson/toJson, also add:
flutter pub add json_annotation
flutter pub add --dev json_serializable
The following versions are added.
dependencies:
freezed_annotation: ^2.2.0
json_annotation: ^4.8.0
dev_dependencies:
freezed: ^2.3.2
json_serializable: ^6.6.1
It seems that the version of json_serializable
requires the following setting in analysis_options.yaml
.
analyzer:
errors:
invalid_annotation_target: ignore
Creating the simplest class with Freezed
The simplest example is the following. `foundation.dart’ is not necessary if it’s not a Flutter project. It must contain factory instead of a constructor.
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'try_freezed.freezed.dart';
@freezed
class FreezedA {
factory FreezedA() = _FreezedA;
}
Note that both import
and part
are mandatory. You should check the official page for the details. Add @freezed
annotation to let Freezed executor know which one is the target class.
Then, run the following commands for auto-generation.
dart run build_runner build
For a Flutter project, use this one instead.
flutter pub run build_runner build
Install Build Runner extension (identifier: gaetschwartz.build-runner
) if you don’t want to run the command every time you change something. It automatically runs the build runner for you.
Then, comments are added automatically to the original code like this.
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'package:freezed_annotation/freezed_annotation.dart';
// required: associates our `main.dart` with the code generated by Freezed
part 'try_freezed.freezed.dart';
@freezed
class FreezedA {
factory FreezedA() = _FreezedA;
}
Then, the auto-generated code is created with try_freezed.freezed.dart
located in the same directory as the original file.
Since toString()
, operator ==
, and get hashCode
are overridden in the generated code. the result will be as follows.
void main() {
print("--- FreezedA ---");
{
final a = FreezedA();
final b = FreezedA();
print(a); // FreezedA()
print(a == b); // true
print(a.hashCode); // 64928094
print(b.hashCode); // 64928094
}
}
Adding final variables to the class
Let’s try to add variables.
@freezed
class FreezedB with _$FreezedB {
const factory FreezedB(
final int age, {
required final String job,
}) = _FreezedB;
}
Don’t forget to add with _$FreezedB
! The following error occurs if it’s not added. I didn’t add it at first and spent hours finding the fact.
$ dart lib/dart/try_freezed.dart
lib/dart/try_freezed.freezed.dart:111:25: Error: The method 'copyWith' isn't defined for the class 'FreezedB'.
- 'FreezedB' is from 'package:flutter_samples/dart/try_freezed.dart' ('lib/dart/try_freezed.dart').
Try correcting the name to the name of an existing method, or defining a method named 'copyWith'.
return _then(_value.copyWith(
^^^^^^^^
lib/dart/try_freezed.freezed.dart:113:20: Error: The getter 'age' isn't defined for the class 'FreezedB'.
- 'FreezedB' is from 'package:flutter_samples/dart/try_freezed.dart' ('lib/dart/try_freezed.dart').
Try correcting the name to the name of an existing getter, or defining a getter or field named 'age'.
? _value.age
^^^
lib/dart/try_freezed.freezed.dart:117:20: Error: The getter 'job' isn't defined for the class 'FreezedB'.
- 'FreezedB' is from 'package:flutter_samples/dart/try_freezed.dart' ('lib/dart/try_freezed.dart').
Try correcting the name to the name of an existing getter, or defining a getter or field named 'job'.
? _value.job
^^^
The result is nice. The property and value are correctly shown and they are used for the object comparison.
final a = FreezedB(25, job: "Programmer");
final b = FreezedB(40, job: "Architect");
final c = FreezedB(40, job: "Architect");
print(a); // FreezedB(age: 25, job: Programmer)
print(a == b); // false
print(b == c); // true
print(a.hashCode); // 234721650
print(b.hashCode); // 428436564
Mutable class wiht unfreezed annotation
@unfreezed
annotation needs to be used if the data class needs to have variables that need to be updated later. The age
is immutable with the final keyword but job
and remark
are defined as mutable variables.
@unfreezed
class FreezedC with _$FreezedC {
factory FreezedC({
required final int age,
required String job,
String? remark,
}) = _FreezedC;
}
The job variable can be updated. "Programmer"
is assigned to the variable b. However, the result of the comparison is false.
final a = FreezedC(25, job: "Programmer");
final b = FreezedC(25, job: "Architect");
print(a); // FreezedC(age: 25, job: Programmer, remark: null)
print(b); // FreezedC(age: 25, job: Architect, remark: null)
print(a == b); // false
b.job = "Programmer";
print(b); // FreezedC(age: 25, job: Programmer, remark: null)
print(a == b); // false <------ it's not true
print(a.hashCode); // 1056331804
print(b.hashCode); // 824381776
This is because hashCode
is not implemented in the class. It seems that it breaks HashMaps/HashSets
if hashCode changes in the object lifetime.
The keys of a HashMap must have consistent Object.== and Object.hashCode implementations. This means that the == operator must define a stable equivalence relation on the keys (reflexive, symmetric, transitive, and consistent over time), and that hashCode must be the same for objects that are considered equal by ==.
https://api.dart.dev/stable/2.19.6/dart-collection/HashMap-class.html
Using Positional parameter
Positional parameter can also be used with freezed annotation but the default value must be specified in this case. The default value can be specified with @Default(<value>)
.
@freezed
class FreezedD with _$FreezedD {
factory FreezedD(int age, [@Default("nothing") String job]) = _FreezedD;
}
If the default value is not needed, @unfreezed
annotation must be used. Remember that hashCode
is not implemented in this case and thus object comparison returns false as explained in the previous section.
final a = FreezedD(25, "Programmer");
final b = FreezedD(40, "Architect");
final c = FreezedD(40, "Architect");
print(a); // FreezedD(age: 25, job: Programmer)
print(a == b); // false
print(b == c); // true
print(a.hashCode); // 530912227
print(b.hashCode); // 443862387
The default value is set in this example, the object comparison returns true if both objects have the same value.
Serialization toJson/fromJson
In some cases, a class needs to be converted to JSON and the other way around. Freezed supports this feature too.
part 'try_freezed.g.dart';
needs to be added in this case to the top of the file. Then, add fromJson
. Don’t forget to add json_serializable
as described in the preparation section.
flutter pub add --dev json_serializable
The code looks like this.
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'package:freezed_annotation/freezed_annotation.dart';
// required: associates our `main.dart` with the code generated by Freezed
part 'try_freezed.freezed.dart';
part 'try_freezed.g.dart';
@freezed
class FreezedE with _$FreezedE {
factory FreezedE(int age, {required String job}) = _FreezedE;
factory FreezedE.fromJson(Map<String, dynamic> json) => _$FreezedEFromJson(json);
}
The result looks as follows.
final a = FreezedE(25, job: "Programmer");
print(a); // FreezedE(age: 25, job: Programmer)
final json = a.toJson();
print(json); // {age: 25, job: Programmer}
final b = FreezedE.fromJson(json);
print(a == b); // true
print(a.hashCode); // 376617188
print(b.hashCode); // 376617188
The class is converted to JSON and a new instance can be created from a JSON.
How to allow update Map/Set/List
If Map/List/Set are used in a freezed class, they can’t be updated.
@freezed
class FreezedF with _$FreezedF {
factory FreezedF({
required Map<String, int> myMap,
required List<int> myList,
required Set<int> mySet,
}) = _FreezedF;
factory FreezedF.fromJson(Map<String, dynamic> json) => _$FreezedFFromJson(json);
}
The last three lines in the following code throw an error.
final a = FreezedF(
myMap: {"one": 1, "two": 2},
myList: [1, 2, 3],
mySet: {9, 8, 7},
);
print(a); // FreezedF(myMap: {one: 1, two: 2}, myList: [1, 2, 3], mySet: {9, 8, 7})
final json = a.toJson();
print(json); // {myMap: {one: 1, two: 2}, myList: [1, 2, 3], mySet: [9, 8, 7]}
final b = FreezedF.fromJson(json);
print(a == b); // true
print(a.hashCode); // 69584972
print(b.hashCode); // 69584972
a.myList.add(4);
a.mySet.add(4);
a.myMap["1"] = 1;
The error is basically the same. UnmodifiablexxxMixin
is thrown.
// for List
Unhandled exception:
Unsupported operation: Cannot add to an unmodifiable list
#0 UnmodifiableListMixin.add (dart:_internal/list.dart:114:5)
#1 main (package:flutter_samples/dart/try_freezed.dart:123:14)
#2 _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:297:19)
#3 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:192:26)
// for Set
Unhandled exception:
Unsupported operation: Cannot modify an unmodifiable Set
#0 UnmodifiableSetMixin._throw (package:collection/src/unmodifiable_wrappers.dart:121:5)
#1 UnmodifiableSetMixin.add (package:collection/src/unmodifiable_wrappers.dart:127:24)
#2 main (package:flutter_samples/dart/try_freezed.dart:124:13)
#3 _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:297:19)
#4 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:192:26)
// for Map
Unhandled exception:
Unsupported operation: Cannot modify unmodifiable map
#0 _UnmodifiableMapMixin.[]= (dart:collection/maps.dart:269:5)
#1 main (package:flutter_samples/dart/try_freezed.dart:125:12)
#2 _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:297:19)
#3 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:192:26)
To update those variables, the annotation needs to change. Beware of that it starts with an uppercase @Freezed
.
@Freezed(makeCollectionsUnmodifiable: false)
class FreezedG with _$FreezedG {
factory FreezedG({
required Map<String, int> myMap,
required List<int> myList,
required Set<int> mySet,
}) = _FreezedG;
factory FreezedG.fromJson(Map<String, dynamic> json) => _$FreezedGFromJson(json);
}
Then, they can be updated like this.
final a = FreezedG(
myMap: {"one": 1, "two": 2},
myList: [1, 2, 3],
mySet: {9, 8, 7},
);
print(a.hashCode); // 410623709
a.myList.add(4);
a.mySet.add(4);
a.myMap["1"] = 1;
print(a); // FreezedG(myMap: {one: 1, two: 2, 1: 1}, myList: [1, 2, 3, 4], mySet: {9, 8, 7, 4})
final json = a.toJson();
print(json); // {myMap: {one: 1, two: 2, 1: 1}, myList: [1, 2, 3, 4], mySet: [9, 8, 7, 4]}
final b = FreezedG.fromJson(json);
print(a == b); // true
print(a.hashCode); // 20025643
print(b.hashCode); // 20025643
The hashCode changes before/after updating the value. It means that the object can’t be used with a hash-based collection.
Comments