Freezed, or even more code generation

Freezed, or even more code generation

Data classes, unions, and fewer lines of code

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.

https://cdn.hashnode.com/res/hashnode/image/upload/v1643935780829/apG4lj7zU.gif

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

https://cdn.hashnode.com/res/hashnode/image/upload/v1643935782813/iwHg5IfxPr.gif

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.

https://cdn.hashnode.com/res/hashnode/image/upload/v1643935784645/aKoHvPFgV.gif