Cleaner Flutter Vol. 4: Hiring repositories

Cleaner Flutter Vol. 4: Hiring repositories

How we define the use of repositories on our projects

Welcome back to Cleaner Flutter, I hope you have been enjoying this series so far and are learning a lot.

💡 Remember to show your support with applause or reaction and share so that more people can have access to this information.

In the previous volume, we talked about entities and started looking at the domain layer. As you see in the title, we're going to continue with repositories... and we continue to make contracts. If you don't know what they are, you can see it in the previous volume.

But before we get into repositories, I want to present something that I hadn't shown you before so you can understand the Clean structure that we are implementing and compare it with some other ones.

CleanScope

This diagram can be expressed as the circles in the original Clean Architecture diagram.

CleanScope Architecture

We have named the architecture with all the packages and configurations of our preference: CleanScope. In the following articles, we're going to delve much more into what we like to use as tools to facilitate development since we know that there are too many options, for example in state management.

The purpose of implementing Clean is that all our layers are independent, but the only layer that has complete autonomy is the domain. This is because the domain defines both the types of data that we are going to use all around our app (entities) and the actions that are going to be carried out in our project (repositories and use cases).

The domain layer defines the general behaviors of our project, let's say all the classes that are the basics for it to work. The data layer comes to use that domain layer, implementing it in the way you want (using a specific package).

For example:

☝️ The domain layer says:

"I have to develop an API that does [x action] and [y action]"

The data layer says:

"I'm going to develop my API using Go with 4 different endpoints, one for..."

This is why we say that our data layer implements the components of the domain layer. But it is important to note that although a domain layer can serve us for different implementations of a data layer, both layers are used when we incorporate them into our presentation layer.

We are going to talk about the data and presentation layer in the following volumes, I just wanted you to observe the general flow. We will dig into the function of each component in the other layers, later. So don't worry.

https://cdn.hashnode.com/res/hashnode/image/upload/v1644200611478/oAlfgXg_1T.gif

Repositories

☝️ Before continuing I want to clarify that, as I said in the first volume, this is one Clean architecture implementation. You can remove and add more layers than those that I am establishing.

Pay attention to this part because you may get a bit confused here.

If you saw in the diagram, the repositories are not totally in the domain layer but are "shared" in turn with the data layer. This is not an error, let's say there are 2 types of repositories:

  1. Repositories in domain layer: They are abstract classes, or contracts, and define the properties and methods that our project will need in a specific feature.
  2. Repositories in the data layer: These are the implementations of the contracts that we define in the domain layer.

That's why we define...

A repository is in charge of retrieving the data to us.

But that can be abstracted a bit more. Probably in other implementations, you will see that the repository calls an API using the models, and that's fine. But there is a detail, the way it calls that external service or API can vary in implementation depending on the package that is used.

For example, I can have a repository that fetches data from an API and I can implement that with Dart's classic package:http. But it might also be that I decide to use package:dio instead of http and my code should be extensible enough to switch those implementations and have no problems.

There we see that abstracting the logic only in repositories to get the data is a bit short. And I clarify that I make this observation for when the project they are building is medium or large, it is not necessary that for each small application you abstract that much.

In CleanScope, we're going to use data sources to get our information with the package we want, but that will be another time.

abstract class IMovieRepository {
  Future<List<Movie>> getLatestMovies();
}

The method doesn't need a body, that is what its implementations take care of.

That is why we created an abstract repository class to define the methods we need to fetch the information we require. I prefix it with I to specify that it is my interface, so when I use my repository in the business logic that is in the presentation layer, it has a shorter and more expressive name.

Consider that there are developers who use the I as a prefix for the implementation and not for the interface, there is no rule for that and it is a discussion that I've had with Elian.

It seems simpler to me since I will only use the interface in my data layer and the presentation layer I will use the implementation in more places, so in that case, a shorter name suits me better.

class MovieRepository extends IMovieRepository {
  @override
  Future<List<Movie>> getLatestMovies() async {
    // Implementation...
  }
}

That's is the implementation of the repository in the data layer, roughly speaking.

Testing

Another reason why it's good to abstract the interface and not code implementation of the MovieRepository directly, is that it makes unit testing of this class much easier.

I know you probably haven't written a lot of tests in Flutter, I can't say I'm very good at it either, further the unit tests I do on my classes and functions. But it doesn't mean that it's not good to make them, conversely, if you still don't do tests (even if they are units) it's time to start.

We all know it already, but it never hurts to mention again that when we create an abstract class, we are creating a contract so that some implementation of that class complies with the established methods.

That's why by having our abstract class, we can extend from the Mock class of package:mocktail --you don't have to use this necessarily, it's only preference--, implement our interface and that our mock or test class has the methods that we set in the contract.

Like...

import 'package:test/test.dart';
import 'package:mocktail/mocktail.dart';

class MockMovieRepository extends Mock implements IMovieRepository { }

void main() {
  // Unit tests...
}

The test class is used as a dependency for a use case, a little spoiler.

And with the help of mocktail and package:test, we can test our repository to confirm that everything works as it should.

⚠️ As usually in the domain or data layer, we don't have Flutter as a dependency. That's why I use package:test, it doesn't include Flutter either and the same as the layers we're dealing with right now, they are written in pure Dart*.

*Unless you use a package that DOES have Flutter as a dependency.

Recap

I wanted to make some points clearer because the domain layer is covered relatively quickly since it's more theory and then defines all the contracts that we're going to have in the project.

We have already seen the first part of the repositories, which is to create its interface or abstract class to later be able to implement it in the data layer (second part) and incidentally, to facilitate testing.

This layering of modules is the SOLID dependency inversion principle mentioned in volume 2 of this series. We're going to use the abstractions that we're creating as domain contracts so that the implementations in the data layer can replace them by being of the same type, so to speak.

And that substitution ability of implementations to their interfaces is precisely Liskov's substitution principle, which was also covered in volume 2.

And so, my dear readers, that's how the SOLID rules are being complied within our layers and we are fully following Clean architecture.

💡 Never forget that you can implement this architecture simpler or more complex, as it suits you, we simply offer you this proposal so that you can learn the concepts and see how we put them into practice.

https://cdn.hashnode.com/res/hashnode/image/upload/v1644200619094/yEX7jf8rW.gif