Freezed, or even more code generation
Data classes, unions, and fewer lines of code
Table of contents
I have already told you about all the benefits and why you should use code generation tools in your projects. If you haven't seen my article on FlutterGen, I'll leave it for you here.
So I'm not going to give you an introduction to generating code, rather we are going to know a very good tool to generate it. This tool is called freezed
.
freezed
comes to be a code generator for immutable classes and was created by Rémi Rousselet, who may know him more from package:provider
.
Immutability
Immutable classes --as the name implies-- are classes that don't change their properties and this feature is very good for state management and for use in data models. Although you may not notice it, you surely have already used immutable classes.
Take a look at this example...
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
home: MyWidget(),
),
);
}
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text(
'Build some widgets!',
style: Theme.of(context).textTheme.headline4!.copyWith(
color: Colors.red,
),
),
),
);
}
}
Here we can see that we access the global theme of our app, specifically the property of the textTheme
or text theme. The headline4
object contains a default setting that cannot be changed, that is, it is immutable.
That's where the copyWith
comes in. This method allows us to return an instance of the same class or an identical object to which we are applying the method, but with the possibility of changing one or more properties as we want.
In the previous example, it allows us to change the color of the headline4
by means ofcopyWith
.
And maybe at this point, you are wondering: why do I want immutable objects if I can easily assign another value to a certain property of the object?
Yes, correct. You can do an assignment easily in this case, but the problem is that the properties are exposed and your code becomes "easy to fool" I would call it. This is used quite a bit in state management because it is much better to change state through functions and not have access to internal properties so as not to change them outside of your class.
This even goes back to the Encapsulation principle of Object-Oriented Programming, where properties must be private and accessed or modified by means of methods. However, here we don't do getters and setters, we allow access to the variables through a specific state that contains them. All this without exposing our logic component.
The last thing I mentioned is very typical of state management using StateNotifier
, which I use with package:riverpod
. In a moment you will understand what I am talking about, if you have not read my article about StateNotifier, I will leave it here.
Value Equality
# Dependencies to include in pubspec.yaml
dependencies:
freezed_annotation:
dev_dependencies:
build_runner:
freezed:
Leaving out a bit of immutability, Freezed not only helps you make objects immutable but also implements value equality.
If you directly compare two equal instances (in properties), when you run the program it will not return true. Look:
class SomeClass {
SomeClass({required this.id});
final int id;
}
void main() {
final first = SomeClass(id: 0);
final second = SomeClass(id: 0);
print(first == second);
}
Run that code on DartPad and you will see that it prints false, even though both objects contain the same id and are essentially the same.
freezed
overrides the equality operator (==
) and the hashCode
getter, that way, we can already compare two or more objects and get true. This is very useful for checks on higher-level layered objects.
We only have to create a class with the decorator @freezed
from the package:freezed_annotation
, so that Freezed knows that it should generate code for this class.
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user.freezed.dart';
@freezed
class User with _$User {
factory User({required String name, required int age}) = _User;
}
Once this abstract class User
is made, we make a mixin with the class that Freezed will generate. We call this generated class _$User
first because we make it private with_
so that it can only be accessed from its own file and we also use$
because it is a convenience to indicate this symbol so that the generated classes don't have the same name of the classes that we create.
Finally, our factory
constructor ofUser
is the class we would use in our code. We can create different constructors like the typical fromJson
and we can also create our own methods. We will see this in serialization, meanwhile, we generate our code.
Generating the code
To generate the code we include a dependency called build_runner
, which is in charge of running the code generators that are in the project. Have a reference build_runner
for other times that use code generators, I didn't recommend it when we talked about FlutterGen because sometimes it can give version problems.
Leave all the dependencies that you include without a specific version, thus avoiding compatibility errors between the versions.
Anyway, the command to run in the root of the project is the following:
# if the project depends on Flutter SDK
flutter pub run build_runner build
# if the project doesn't depend on Flutter - if it's pure Dart
pub run build_runner build
Every time you make a change to your classes you will have to run one of these commands to see the changes in your generated code, in case there are errors.
As a small note, if your project uses a linter like very_good_analysis
, I suggest you add these lines to your analysis_options.yaml
, that way you don't have errors in your generated files:
analyzer:
exclude:
- "**/*.g.dart"
- "**/*.freezed.dart"
Serialization
Data serialization is an essential part of the models since most apps contact third-party services or custom APIs that return responses in JSON format.
We decode these JSONs thanks to the dart:convert
module that is already included in Dart, but then we have the need to locate each property of the decoded map that we have left in a data model.
That's where the fromJson
constructor comes into play in our classes, which allows us to translate our map into a class, we add it like this:
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user.freezed.dart';
part 'user.g.dart';
@freezed
class User with _$User {
factory User({required String name, required int age}) = _User;
// fromJson constructor
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
We add a new part
directive that will be from the JSON serializable file, and then we add the new factory
that we will have for serialization.
freezed
doesn't do this serialization to JSON on its own, it needs us to include the package json_serializable
thedev_dependencies
from pubspec.yaml
.
It can go under freezed
.
I have already presented you with many of the very powerful main features that freezed
has, now we are going to the last one that I think is one of the coolest that saves many lines of code.
Unions
Unions are a feature that Freezed brings to Dart from other languages like Kotlin. In Kotlin they are called sealed classes. Even the syntax that Freezed offers is very similar.
They are classes that implement a certain abstract class and include a "switch" in their parent class to define actions based on what implementation it is.
This last detail reduces the code a lot and I personally use it to define the states of my logic component. Something like this:
import 'package:freezed_annotation/freezed_annotation.dart';
part 'common_state.freezed.dart';
@freezed
class CommonState with _$CommonState {
const factory CommonState(int value) = Data;
const factory CommonState.loading() = Loading;
const factory CommonState.error([String? message]) = ErrorDetails;
}
And then we can use the when
ormaybeWhen
methods to perform an operation based on any of those cases, like this:
const state = CommonState(42);
state.maybeWhen(
null,
loading: () => 'loading',
error: (error) => '$error',
orElse: () => 'fallback',
);
One of the most popular dependency injection and state management packages, riverpod
, uses freezed
to define its providers as sealed classes and is thus much easier in syntax handling widgets in Flutter that reacts to a specific state.
But as I always say, read the documentation, this is a small introduction so that you know what it is about, what you can do, and above all, lose your fear to explore it.