Represent the data coming from a REST API with freezed

Use the power of code generators

This year in the February challenge at flutterchallenge.dev the goal was to build a Tinder-like application for cats and dogs by using a REST API to fetch data. This is the first part of a series that shows an implementation for this challenge and now we are going to create our data classes to represent the data coming from the REST endpoints with the help of the package freezed.

freezed is a code generator tool that helps you build your data classes with tons of helpful features, like the copyWith() or fromJson() methods.

Add the dependencies

So let's start with a fresh new Flutter project and add freezed to the dependencies:

# pubspec.yaml
dependencies:
  freezed_annotation: ^1.1.0
  json_annotation: ^4.4.0

dev_dependencies:
  build_runner: ^2.1.8
  freezed: ^1.1.1
  json_serializable: ^6.1.5

The freezed_annotations will help us mark our classes so the freezed package will know which classes need to be generated when we run build_runner. If we add json_annotation and json_serializable to the dependencies as well, freezed can generate the toJson() and fromJson() methods as well.

Create our data representation

Let's see what we the REST API provides us. The challenge provides a swagger documentation about the available endpoints. Our goal is to have a class for each endpoint to represent its JSON response in our app.

Screenshot 2022-04-03 at 8.53.14.png

Model the API with freezed

Let's start with the /cats endpoint which will return an array of cats in the following format

[
  {
    "id": "9eef0518d8b5a333ee1fcb1a06a1474a23192c6a",
    "name": "Luna",
    "path": "/cats/cat001.jpeg"
  }
]

So our Cat class should have 3 fields. This is how it could be created with the help of freezed. Let's create a cat.dart file under the folder lib/features/animals/models.

import 'package:freezed_annotation/freezed_annotation.dart';

// This file contains the generated code by freezed.
part 'cat.freezed.dart';
// This file contains the generated code by json_serializable.
part 'cat.g.dart';

// We mark the class with @freezed annotation so freezed knows it has 
// some to work to do.
// _$Cat is a generated class which contains all the constructors, 
// methods that makes our class immutable.
@freezed
class Cat with _$Cat {
  // With the help of this factory constructor we define the fields 
  // of our class.
  // _Cat is a class that will hold the implementation details. 
  const factory Cat({
    required String id,
    required String? name,
    required String path,
  }) = _Cat;
  // Adding this factory will instruct freezed and json_serializable to 
  // generate the fromJson() and toJson() methods for us.
  factory Cat.fromJson(Map<String, dynamic> json) => _$CatFromJson(json);
}

Now we have to run build_runner to generate the code so the cat.freezed.dart and cat.g.dart files are created and all the errors are eliminated from our IDE.

You can generate the code once by running

flutter pub run build_runner build --delete-conflicting-outputs

But if you are developing an app you can watch for changes and always generate the code without running manually the build_runner.

flutter pub run build_runner watch --delete-conflicting-outputs

build_runner will scan your codebase and if there is a code generator that finds something to work on, it will execute it.

Let's move on to the next model, to the Vote. The /votes, and /votes/{id} endpoints. Whenever we get, create or modify a vote, the API responses with a structure like this.

{
  "id": 1,
  "user_id": 1,
  "vote_type": "cat",
  "animal_id": "9eef0518d8b5a333ee1fcb1a06a1474a23192c6a",
  "liked": true,
  "created_at": "2022-01-07T11:39:40.992Z",
  "updated_at": "2022-01-07T11:39:40.992Z"
}

We can do the same as we did with the cats.

import 'package:freezed_annotation/freezed_annotation.dart';

part 'vote.freezed.dart';
part 'vote.g.dart';

@freezed
class Vote with _$Vote {
  const factory Vote({
    required int id,
    required int user_id,
    required String vote_type,
    required String animal_id,
    required bool liked,
    required DateTime created_at,
    required DateTime updated_at,
  }) = _Vote;

  factory Vote.fromJson(Map<String, dynamic> json) => _$VoteFromJson(json);
}

In addition to the basic int and String types, the json_serialiable can handle DateTime as well. We have the part definitions, the class with the mixin and the factory method with the fields we want in our class along with the fromJson.

Represent the HTTP request body

When we want to create or modify a vote, the POST /votes and PUT /votes/{id} endpoints have a request body, which is basically a JSON with specific fields. We can represent this JSON the same way as we did with the data we are getting back from the API. So this

{
  "vote_type": "cat",
  "animal_id": "9eef0518d8b5a333ee1fcb1a06a1474a23192c6a",
  "liked": true
}

can be managed by

import 'package:freezed_annotation/freezed_annotation.dart';

part 'vote_param.freezed.dart';
part 'vote_param.g.dart';

@freezed
class VoteParam with _$VoteParam {
  const factory VoteParam({
    required String vote_type,
    required String animal_id,
    required bool liked,
  }) = _VoteParam;

  factory VoteParam.fromJson(Map<String, dynamic> json) =>
      _$VoteParamFromJson(json);
}

Following the above, we can represent our data in our app easily. If you don't want to do this manually and you are using VSCode, I can recommend the Json to Dart Model extension, which can generate your data classes from a JSON string and it can work with freezed.

Fix the linting errors

You'll get lint errors for the Vote class because of the variable naming

Name non-constant identifiers using lowerCamelCase.

You can fix this by using the JsonKey annotation like this

// ignore_for_file: invalid_annotation_target

import 'package:freezed_annotation/freezed_annotation.dart';

part 'vote_param.freezed.dart';
part 'vote_param.g.dart';

@freezed
class VoteParam with _$VoteParam {
  const factory VoteParam({
    @JsonKey(name: 'vote_type') required String voteType,
    @JsonKey(name: 'animal_id') required String animalId,
    required bool liked,
  }) = _VoteParam;

  factory VoteParam.fromJson(Map<String, dynamic> json) =>
      _$VoteParamFromJson(json);
}

By default, you'll get another lin error due to this issue, you can safely ignore it by putting the following at the top of your file.

// ignore_for_file: invalid_annotation_target

Summary

By using code generation, you can quickly add the data classes to your project to model a REST API. The good thing is that you'll get lots of features by default that can improve the quality of your projects in the long run and it can speed up your development time since you don't have to write the code by yourself.

You can find the relevant source code here.

In the next part of the series, I'll introduce the communication with the REST API with the help of the chopper package.