Flutter Freezed autogenerates code for copyWith, serialization, and equal

eye-catch Dart and Flutter

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
Sponsored links

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
Sponsored links

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

Copied title and URL