Flutter Architecture: repository-service pattern
🕑 6 min read
The repository-service is not a new pattern, but it is powerful. I have personally used it for quite some time, and now I wanted to share it with you all.
In this post, we will see real examples of implementing the repository-pattern and also learn some pros and cons the pattern has.
Some background
To understand the repository-service pattern, we first need to understand what a repository and what a service is.
- A repository is a class that will handle getting data into and out of your
database (data store). For example, if you have a
Dog
model, you will probably also have aDogRepository
. - A service is a class that will combine multiple repositories to provide more
complex domain data. For example, if we have a
Dog
and aCatRepository
, we might have anAnimalService
to get all our animals and aggregate data.
You might already imagine a con of this pattern and, that is "boiler-plate" code, or, in simple terms, a bunch of god damn code 😡.
The practical application
Let's move away from the background and get into the coding!
To better explain this, we will keep the animals. I love animals, why not use them to explain some code!
Below we have two endpoints, one for dog
and one for cat
. I made them in
singular, to better grasp the concept.
{
"id": 1,
"name": "Fluffy",
"species": "cat",
"age": 2,
"breed": "persian"
}
{
"id": 1,
"name": "Bobby",
"species": "dog",
"age": 4,
"breed": "Keeshound"
}
The repository in repository-service pattern
I will ignore the part where we create the models/entities for the above JSON, just imagine for now that we have it.
Now two typical repositories would look something like this
class DogRepository {
Future<Dog> getDog() async {
// Do the required code to fetch it
// with whatever package or solution you want.
try {
final dog = await fancyFetchLogicForDog();
return dog;
} catch (e){
throw CustomException(e);
}
}
}
class CatRepository {
Future<Cat> getCat() async {
// Do the required code to fetch it
// with whatever package or solution you want.
try {
final cat = await fancyFetchLogicForCat();
return cat;
} catch (e){
throw CustomException(e);
}
}
}
✅ Simplicity
Notice that both our repositories are very simple in terms of implementation. This is one of the reasons why I love repositories.
Now that we have done the above code, we just need to also create the services. Yes... that is right, one of the cons is now incoming...
We need to create 3 services:
DogService
CatService
AnimalService
The reason for this is because depending on what we want in the application the layer we want to use the appropriate service.
Now how does these look?
🧙 The magic of services
I will keep it simple and ignore the DogService
and CatService
for now. The
reason for this is because they will simply call respective repository and
potential error handling, conversion, etc.
Instead let's focus on the AnimalService
!
class AnimalService {
final DogRepository _dogRepository;
final CatRepository _catRepository;
AnimalService(this._dogRepository, this._catRepository);
Future<List<Animal>> getAllAnimals() async {
try {
final dog = await _dogRepository.getDog();
final cat = await _catRepository.getCat();
return [dog, cat];
} on CustomException catch (e) {
// Example will just rethrow the same exception.
// I personally love using https://pub.dev/packages/multiple_result
// and return a custom failure.
rethrow;
}
}
Future<double> getAverageAnimalAge() async {
try {
final dog = await _dogRepository.getDog();
final cat = await _catRepository.getCat();
final animals = [dog, cat];
final averageAge = animals.map((animal) => animal.age).reduce((a, b) => a + b) / animals.length;
return averageAge;
} on CustomException catch (e) {
// Example will just rethrow the same exception.
// I personally love using https://pub.dev/packages/multiple_result
// and return a custom failure.
rethrow;
}
}
}
On purpose, I didn't do any kind of error handling to keep the code to a minimum. But if you would like to see that, make sure to share the article. This is something I cover in-depth in my course, but it requires a quite large blog post! For something free, I made a personal project open-source that implements this pattern.
Get my free Flutter Tips PDF
Level up your skills by joining 1600+ other Flutter developers!
The end result
Thanks to this pattern we have gotten a clean way to interact with our backend data.
// Imagine this has some fancy ui and this would be the method call.
Future<void> getAverageAnimalAge() async {
// The service would do all the heavy lifting of converting the data
// to the required response. If you used https://pub.dev/packages/multiple_result
// you can here, handle that failure.
final averageAnimalAge = await animalService.getAverageAnimalAge();
setState(() {
_averageAnimalAge = averageAnimalAge;
});
}
But what I love the most is the freedom to do our error handling just as we want and still keep the calling side clean.
Some notes from feedback
There are some confusion for people with Android background. I want to personally quote something that I wrote about in this github issue.
"I personally follow DDD: View - Controller - Application Service - Repository
To me, this has made the most sense and has made my code the most decoupled. The reason I've gone with this is in my experience a lot of people coming from other technologies has a base understanding that a Repository handles crud operations while Service handles the business request. I.e I need x from x repository but also y from y repository, put these together and we have that value xy in the service.
The controller would then take the combined value (xy) from the service.
Now there are also times when a Repository is not needed or a Service is not needed but would only clutter. But I think we should be careful when going for the Android style as there are not only people coming from an Android background and to them a Repository calling a Service does not make sense to the definitions of the words. But to others this make a complete sense.
I just want to add that I don't think any of the approaches are bad but just wanted to give an additional opinion 😊"
You can, of course, have another "DataStore" layer that the repository have access too!
Ending gratitude
I would like to both shout-out Andrea Bizotto from with his astounding article on the repository pattern
And Matthew Jones about his service-repository article that I highly recommend reading through.