Freezed - Easy data classes and unions

Freezed allows us to write minimal code by hand and uses code generation to take care of the rest for implementing data classes and unions.

Blog Cover Image
Moksh Mahajan

Moksh Mahajan

Calender

September 23, 2022

Dart is a very powerful and robust modern programming language. But still, there are some features like the Data classes and the Sealed classes that are integral to modern-day development but are not currently present, resulting in writing a lot of boiler-plate to implement simple things like comparing two objects of the same class and exhaustive switches.

What is a Data Class?

While building apps we have to deal with a lot of data. We need to transfer data to and from different layers of our app. For example, consider the data related to a User. Among other things, the user may have a username, an email, and a profile picture URL associated with it. It makes no sense to transport these values among the different app layers as individual loose values. Instead, we should create a class that bundles all of the related data fields and use the resulting object for the data transfer.

class User {
 final String userName;
 final String email;
 final String imageUrl;
 
 const User({
   required this.userName,
   required this.email,
   required this.imageUrl,
 });

This kind of class is referred to as a data class. But just having a regular class for this purpose is not enough.

A data class should have these features:

  1. An over-ridden equality operator so that we can compare objects based on their values instead of reference. Dart by default supports referential equality and not value equality.
  2. A copyWith method that facilitates deep copying or cloning
  3. An over-ridden toString method that makes debugging easy.

Taking care of all these properties and creating data classes manually would take a lot of time and make the file containing our class quite messy and more susceptible to errors and this is where freezed comes in.

Without Freezed

@immutable
class User {
 final String userName;
 final String email;
 
 const User({
   required this.userName,
   required this.email,
 });
 
 @override
 String toString() => 'User(userName: $userName, email: $email)';
 
 @override
 bool operator ==(other) =>
     other is User &&
     other.runtimeType == runtimeType &&
     other.userName == userName &&
     other.email == email;
 
 @override
 int get hashCode => Object.hash(runtimeType, userName, email);
 
 User copyWith({String? userName, String? email}) => User(
       userName: userName ?? this.userName,
       email: email ?? this.email,
     );
 
 factory User.fromJson(Map<String, dynamic> json) => User(
       userName: json['userName'] as String,
       email: json['email'] as String,
     );
 
 Map<String, dynamic> toJson() => {
       'userName': userName,
       'email': email,
     };
}

With Freezed

@freezed
class User with _$User {
 const User._();
 const factory User({
   required this.userName,
   required this.email,
 }) = _User;
 
 factory User.fromJson(Map<String, dynamic> json)  => _$UserFromJson(json);
}

What is Freezed?

Freezed allows us to write minimal code by hand and uses code generation to take care of the rest for implementing data classes and unions. (We’ll be explaining about unions in a future article so, fear not.)

Steps for creating a data class using Freezed

Add all the above mentioned dependencies and dev-dependencies in the pubspec.yaml. (Note: We can use the flutter pub add <dependency_name> command to add them)

dependencies:
 flutter:
   sdk: flutter
 freezed_annotation: ^2.0.3

dev_dependencies:
 flutter_test:
   sdk: flutter
 build_runner: ^2.1.11
 freezed: ^2.0.3+1

Freezed is used only for code generation, so has to be added as a dev-dependency. build_runner needs to be added as a dev-dependency as well since we’re dealing with generating files using Dart code. And at last, freezed_annotation needs to be added as a normal dependency in which the annotations are defined for the freezed to generate code.

Create a dart file for the data class (say user.dart).

Add the minimal code mentioning just the fields that the user class should contain.

part 'user.freezed.dart';

@freezed
class User with _$User {
 const User._();
 const factory User({
   required this.userName,
   required this.email,
 }) = _User;
}

The part statement on the top is for linking the generated freezed file, that we’re going to generate in the next step. Run the build_runner command. It will generate the freezed file for our data class which will contain all the implementation details for the value equality, copyWith, toString and other aspects of a data class.

flutter pub run build_runner build

Adding JSON serialization to the data class

JSON is currently the most popular data format which is used in client-server communication. So, extending our data class with JSON serialization and de-serialization capabilities definitely makes sense.

Serializing data to and from JSON is a cake walk with json_serializable package. And, thankfully, freezed has a good integration with it.

First, we need to add json_serializable as a dev_dependency (for code generation).

dev_dependencies:
 flutter_test:
   sdk: flutter
 build_runner: ^2.1.11
 freezed: ^2.0.3+1
 json_serializable: ^6.1.4

Next, we just need to add a little boiler-plate in order to trigger json_serializable’s generator.

part 'user.freezed.dart';
part 'user.g.dart';
				
@freezed
class User with _$User {
 const User._();
 const factory User({
   required this.userName,
   required this.email,
 }) = _User;
	
 factory User.fromJson(Map<String, dynamic>      json) => _$UserFromJson(json);
}

Just a part statement to link the generated file containing all the Json serialization logic and a one-liner fromJson factory method referencing the class generated by the json_serializable’s generator. This will generate all the necessary code to call toJson as an instance method and fromJson as a factory.

What are Unions and how to implement them using Freezed package?

The blog on unions is coming soon….