DEV Community

loading...
Cover image for Cleaner Flutter Vol. 6: Modeling some data

Cleaner Flutter Vol. 6: Modeling some data

marcossevilla profile image Marcos Sevilla ・7 min read

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 datasources that make the request for data directly and finally we call a datasource 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 datasources are a single class.

The datasources 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://media.giphy.com/media/l0HlFZ3c4NENSLQRi/giphy.gif

Models

I suppose you have already defined some folder in your projects called models. Here there really 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 own REST API.

StoreItem Entity

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

StoreItemAPIModel

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 with respect to the entity.

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

StoreItemFirebaseModel

It's exactly 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

The first package I'm going to talk you about is 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';
import 'package:meta/meta.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];
}
Enter fullscreen mode Exit fullscreen mode

Once our entity is created, I suggest you combine Equatable with json_serializable and 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 'package:meta/meta.dart';

import 'store_item_equatable.dart';

part 'store_item_model.g.dart';

@JsonSerializable(nullable: false)
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);
}
Enter fullscreen mode Exit fullscreen mode

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

# si el proyecto tiene dependencia en el SDK de Flutter
flutter pub run build_runner build

# si el proyecto no tiene dependencia de Flutter - si estΓ‘ escrito en Dart puro
pub run build_runner build
Enter fullscreen mode Exit fullscreen mode

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());
  }
}
Enter fullscreen mode Exit fullscreen mode

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, 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
abstract 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());
  }
}
Enter fullscreen mode Exit fullscreen mode

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 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'},
  );
}
Enter fullscreen mode Exit fullscreen mode

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 my article that I quoted earlier, such as the when, maybeWhen, map, maybeMap.

We love Freezed, in fact 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 combination with json_serializable.

https://media.giphy.com/media/FA77mwaxV74SA/giphy.gif

As always...

You can share this article to help another developer to continue improving their productivity when writing applications with Flutter.

There's a Spanish version of this article in Medium. You're welcome. πŸ‡ͺπŸ‡Έ

Also if you liked this content, you can find even more and keep in contact with me on my socials:

Discussion (0)

pic
Editor guide