Cleaner Flutter Vol. 6: Modeling some data

Cleaner Flutter Vol. 6: Modeling some data

Turning entities to models

Once the domain layer and all the general behaviors of our project are finished through abstraction, we can start using all those interfaces that we made to create their implementations.

Well, finally we change layers. This time it's up to the data layer, starting with the models.

Data layer

Data Layer

In the domain layer, the base were entities. These defined the proper data types in the project and their essential properties.

In the data layer it happens in the same way, we must establish the types of data that we are going to manipulate in the methods that a repository (now implemented) uses when fetching data.

💡 In this volume, we're going to make a lot of reference to volume 3. If you haven't read it, here you go.

In this new layer, we have our components that already communicate directly with an external API of any type, be it Firebase, a REST API, etc.

The construction of the data layer is based on first creating the models, then we create the data sources that request data directly and finally we call a data source from the repository to return information, either in primitive data types or models.

I clarify that repositories are implementations of other abstract repositories and models are inheritors of entities (which are classes as such, not interfaces), but that does not mean that data sources are a single class.

The data sources must also have an abstract class to be able to do the same as with the repositories: be able to implement its interface with a different package without affecting any external layer and to be able to perform better tests based on the interface.

Let's get to the models.

https://cdn.hashnode.com/res/hashnode/image/upload/v1644291473711/MJXPKMGXU.gif

Models

I suppose you have already defined some folders in your projects called models. Here there isn't much difference, with our entities we wanted to leave as little as possible in properties and methods because our models were going to be in charge of adding that functionality.

A very famous constructor method in Flutter and Dart is the fromJson. This constructor allows us to parse a decoded JSON to a Map and thus serialize our data when we construct the object.

This type of functionality is what a model contains since we can have multiple models that inherit from that entity and define a different behavior based on its implementation.

It also allows us to have entities that can be used regardless of whether it contacts a backend hosted in Firebase or if it is its REST API.

class StoreItem {
  StoreItem({
    required this.id,
    required this.name,
  });

  final int id;
  final String name;
}

StoreItem will be the entity we use as an example. We're going to make our models based on what this entity defines, then we first create a model for the case when it comes from an API, which we are going to name StoreItemAPIModel.

class StoreItemModel extends StoreItem {
  StoreItemModel({
    required int id,
    required String name,
    required this.isAvailable,
  }) : super(id: id, name: name);

  factory StoreItemModel.fromJson(Map<String, dynamic> json) {
    return StoreItemModel(
      id: json['id'],
      name: json['name'],
      isAvailable: json['isAvailable'],
    );
  }

  final bool isAvailable;
}

We only need to extend or inherit from StoreItem to have all our properties.

Notice that the constructor creates the parameters within itself and does not declare properties that we already defined in the entity, we only have to send these variables in the constructor to the parent class called super.

We also add an extra constructor, the popular fromJson. This constructor is the additional functionality that our model will contain for the entity.

And if we want to make another model with the same properties of our entity, it is very easy for us.

import 'package:cloud_firestore/cloud_firestore.dart';

class StoreItemModel extends StoreItem {
  StoreItemModel({
    required int id,
    required String name,
    required this.isAvailable,
  }) : super(id: id, name: name);

  factory StoreItemModel.fromJson(Map<String, dynamic> json) {
    return StoreItemModel(
      id: json['id'],
      name: json['name'],
      isAvailable: json['isAvailable'],
    );
  }

  factory StoreItemModel.fromFirestore(DocumentSnapshot snapshot) {
    return StoreItemModel.fromJson(snapshot.data() as Map<String, dynamic>);
  }

  final bool isAvailable;
}

It's the same as we do with the other one but it has different methods since it is oriented to a different service. Similarly, the isAvailable variable that we define remains only in our model, so we must declare it outside the constructor.

Another thing that we can remark from this model is that we must also implement another fromJson because it doesn't extend from our previous class and the constructors remain at the model level.

This last action can be somewhat annoying and repetitive. To use the fromJson constructor we would have to inherit from StoreItemAPIModel, but it still gets complicated when calling the other constructor. From this last paragraph, stick with the first statement and ignore the rest as it is a complete mess.

Best ways to create models

The way we saw for creating models previously it's plenty for us. But this doesn't mean that the same cannot be automated. There are two packages that I use a lot for my models and in general, any immutable class that it has (a state, for example).

Equatable + json_serializable

I'm going to talk to you about package:equatable. So far this is the package with which I make entities and, consequently, models. It allows us to create a class whose properties don't change, in addition to giving us a method that we must overwrite called props, this returns a list with the properties we want of the class. Very useful and complete.

import 'package:equatable/equatable.dart';

class StoreItemEquatable extends Equatable {
  StoreItemEquatable({
    required this.id,
    required this.name,
  });

  final int id;
  final String name;

  @override
  List<Object> get props => [id, name];
}

Once our entity is created, I suggest you combine Equatable with package:json_serializable and package:json_annotation to create your models with the fromJson constructor and thetoJson method to be able to serialize from any API that returns in JSON format.

import 'package:json_annotation/json_annotation.dart';

import 'store_item_equatable.dart';

part 'store_item_model.g.dart';

@JsonSerializable()
class StoreItemModel extends StoreItemEquatable {
  StoreItemModel({
    required int id,
    required String name,
  }) : super(id: id, name: name);

  factory StoreItemModel.fromJson(Map<String, dynamic> json) {
    return _$StoreItemModelFromJson(json);
  }

  Map<String, dynamic> toJson() => _$StoreItemModelToJson(this);
}

I remind you that json_serializable must generate the code in a file with the extension **.g.dart. To generate the code, you must run the command:

# if your project depends on the Flutter SDK
flutter pub run build_runner build

# if your project doesn't depend on the Flutter SDK, it is written in pure Dart
pub run build_runner build

Likewise, if we want to create our model for Firestore, we just extend the model we created earlier to use its fromJson constructor and that's it.

import 'package:flutter/material.dart';

import 'package:cloud_firestore/cloud_firestore.dart' show DocumentSnapshot;

import 'store_item_model.dart';

class StoreItemFirestore extends StoreItemModel {
  StoreItemFirestore({
    required int id,
    required String name,
  }) : super(id: id, name: name);

  factory StoreItemFirestore.fromFirestore(DocumentSnapshot doc) {
    return StoreItemModel.fromJson(doc.data()!);
  }
}

Although Equatable checks the essentials, and well, it doesn't include an important method of immutable classes: copyWith. For this reason, I'm going to show you the second alternative.

Freezed

To solve the problem of how long it takes to create all those constructors, package:freezed is a great option. To know more about this package, you can see my article here.

freezed includes the following:

  • Immutability (copyWith included).
  • Value equality.
  • Unions.
  • Support for json_serializable.

So we can define all the models in the same abstract class and use the methods on themselves like this...

import 'package:cloud_firestore/cloud_firestore.dart' show DocumentSnapshot;
import 'package:freezed_annotation/freezed_annotation.dart';

part 'store_item.freezed.dart';
part 'store_item.g.dart';

@freezed
class StoreItem with _$StoreItem {
  const factory StoreItem.model({
    required int id,
    required String name,
  }) = StoreItemModel;

  const factory StoreItem.firestore({
    required int id,
    required String name,
    required bool isAvailable,
  }) = StoreItemFirestore;

  factory StoreItem.fromJson(Map<String, dynamic> json) =>
      _$StoreItemFromJson(json);

  factory StoreItem.fromFirestore(DocumentSnapshot doc) {
    return StoreItemFirestore.fromJson(doc.data());
  }
}

But not everything is pretty on Freezed, do you notice the problem?

Sure, unions are great for creating all possible constructors without having to create several separate files. But this separation of classes and files is what allows us to have separate entities and models.

A freezed class cannot be a subclass of another and try to extend the functionality of a freezed class is a time investment that isn't worth it and we lose the time we saved generating the code.

Equatable or Freezed?

import 'package:models_sample/store_item.dart' as equatable;
import 'package:models_sample/store_item_model.dart' as freezed;

void main() {
  final aModel = equatable.StoreItemModel.fromJson(
    {'id': 0, 'name': 'Computer'},
  );

  final bModel = freezed.StoreItemModel.fromJson(
    {'id': 0, 'name': 'Computer'},
  );
}

It really has been a matter of personal preference since the result is very similar. But in the case of CleanScope, we prefer to use equatable for entities and models, as it allows us better abstraction and control over the code.

Equatable has the props method which is very useful in many cases. freezed has built-in the copyWith and many other methods that I detail in the article that I quoted earlier, such as the when, maybeWhen, map, maybeMap.

We love freezed we use it to generate the states for the logic in the presentation layer, but it doesn't satisfy us for the level of attraction we need in the models. It's a limitation with code generators, they take away control over your code to a certain extent.

When we finish this series of articles, we'll propose a minimalist version of CleanScope where we'll use freezed for the models as we will have fewer layers of abstraction. But that's going to be another time.

For the moment, we're sticking with equatable for our models, in conjunction with json_serializable.

https://cdn.hashnode.com/res/hashnode/image/upload/v1644291482087/H-u9QuXBp.gif