Repository is the brain of the data layer of an app. It handles data from remote and local Data Sources, decides which Data Source to prefer and also, this is where data caching policy is decided upon.
In the previous part, we've gone over the basic structure of the data layer and today, it's time to start implementing the data layer right from its core - from the NumberTriviaRepository, all the while creating contracts for its dependencies.
Implementing the Contract
Interfaces, abstract classes,... whatever. Every language has its own spin on it. The important thing is that we already have a contract which the Repository implementation must fulfill. This way, the Use Cases communicating with the Repository don't have to know a thing about how to it operates.
Let us, therefore, create a new file under data/repositories for the number_trivia feature, which will contain a concrete class NumberTriviaRepositoryImpl.
number_trivia_repository_impl.dart
import 'package:dartz/dartz.dart';
import '../../../../core/error/failure.dart';
import '../../domain/entities/number_trivia.dart';
import '../../domain/repositories/number_trivia_repository.dart';
class NumberTriviaRepositoryImpl implements NumberTriviaRepository {
@override
Future<Either<Failure, NumberTrivia>> getConcreteNumberTrivia(int number) {
// TODO: implement getConcreteNumberTrivia
return null;
}
@override
Future<Either<Failure, NumberTrivia>> getRandomNumberTrivia() {
// TODO: implement getRandomNumberTrivia
return null;
}
}
At the risk of sounding like a broken record, I'll say it again - the Repository needs lower level Data Sources to get the actual data from.
Repository Dependencies
In this part, we will only create contracts for all the Repository dependencies. This will allow us to mock them easily without needing to bother with implementing them just yet - that's coming in the next parts.
Are Data Sources enough though? After all, we will cache the latest NumberTrivia locally to make sure the user sees something even when he's offline. This means, we'll also need to have a way to find out about the current state of the network connection. Since we want to keep our code as independent from the outside world as possible, we won't just plop any 3rd party library for connectivity directly into the Repository. Instead, we'll create a NetworkInfo class.
Network Info
This class will live inside network_info.dart file under the core/platform folder. That's because it deals with the underlying platform - on Android, the process of getting network info can be different than on iOS. Of course, we will use a 3rd party package to unify the process, but just in case...
network_info.dart
abstract class NetworkInfo {
Future<bool> get isConnected;
}
Remote Data Source
The public interface of NumberTriviaRemoteDataSource will be almost identical to the one of the Repository - it will have methods getConcreteNumberTrivia and getRandomNumberTrivia. As we discussed in the previous part though, the return type will be different.
We're on the boundary between the outside world and our app, so we want to keep this simple. There will be no Either<Failure, NumberTrivia>, but instead, we're going to return just a simple NumberTriviaModel (converted from JSON). Errors will be handled by throwing Exceptions. Taking care of this "dumb" data and converting it to the Either type will be the responsibility of the Repository.
number_trivia_remote_data_source.dart
import '../models/number_trivia_model.dart';
abstract class NumberTriviaRemoteDataSource {
/// Calls the http://numbersapi.com/{number} endpoint.
///
/// Throws a [ServerException] for all error codes.
Future<NumberTriviaModel> getConcreteNumberTrivia(int number);
/// Calls the http://numbersapi.com/random endpoint.
///
/// Throws a [ServerException] for all error codes.
Future<NumberTriviaModel> getRandomNumberTrivia();
}
Adding Exceptions and Failures
As you can see in the documentation, both of these methods will throw a ServerException when the response doesn't have a 200 OK code, but, for example, a 404 NOT FOUND code. The thing is, we currently don't have any ServerException, so let's create it in order to use it in the Repository tests.
The ServerException can be potentially shared across multiple features, so we're going to put it inside core/error/exception.dart file. While we're at it, we're also going to create a CacheException which will be thrown by the local Data Source.
exception.dart
class ServerException implements Exception {}
class CacheException implements Exception {}
Since we're dealing with Exceptions, let's also get the Failures out of the way. Remember, the Repository will catch the Exceptions and return them using the Either type as Failures. For this reason, Failure types usually exactly map to Exception types.
failure.dart
import 'package:equatable/equatable.dart';
abstract class Failure extends Equatable {
Failure([List properties = const <dynamic>[]]) : super(properties);
}
// General failures
class ServerFailure extends Failure {}
class CacheFailure extends Failure {}
Local Data Source
Up until now, the methods we created were always about getting data, whether the Entity or the Model. They were also split into getting concrete or random number trivia. We're going to break this pattern with the NumberTriviaLocalDataSource.
Here, we will also need to get the data into the cache and we're also not going to care whether we're dealing with concrete or random number trivia. That's because the caching policy (implemented inside the Repository) will be simple - always cache and retrieve the last trivia gotten from the remote Data Source.
number_trivia_local_data_source.dart
import '../models/number_trivia_model.dart';
abstract class NumberTriviaLocalDataSource {
/// Gets the cached [NumberTriviaModel] which was gotten the last time
/// the user had an internet connection.
///
/// Throws [NoLocalDataException] if no cached data is present.
Future<NumberTriviaModel> getLastNumberTrivia();
Future<void> cacheNumberTrivia(NumberTriviaModel triviaToCache);
}
Future<NumberTriviaModel> getConcreteNumberTrivia(int number);
Setting Up the Repository
While we're not going to write any actual logic of the NumberTriviaRepositoryImpl class in this part (that's coming in the next one!), we will at least set it up to be prepared to work with all its dependencies we created above. Since we're doing TDD, we will also prepare the test file for the next part.
Again, in the spirit of TDD, we will write the test first, although we won't really test anything just yet. As usual, the test file goes into the "mirror image" location of the production file.
We know that the Repository should take in the remote and local Data Sources and also a NetworkInfo object. Because we're preparing the test file for the next part, let's create Mocks for these dependencies straight away.
number_trivia_repository_impl_test.dart
class MockRemoteDataSource extends Mock
implements NumberTriviaRemoteDataSource {}
class MockLocalDataSource extends Mock implements NumberTriviaLocalDataSource {}
class MockNetworkInfo extends Mock implements NetworkInfo {}
void main() {
NumberTriviaRepositoryImpl repository;
MockRemoteDataSource mockRemoteDataSource;
MockLocalDataSource mockLocalDataSource;
MockNetworkInfo mockNetworkInfo;
setUp(() {
mockRemoteDataSource = MockRemoteDataSource();
mockLocalDataSource = MockLocalDataSource();
mockNetworkInfo = MockNetworkInfo();
repository = NumberTriviaRepositoryImpl(
remoteDataSource: mockRemoteDataSource,
localDataSource: mockLocalDataSource,
networkInfo: mockNetworkInfo,
);
});
}
What's that? Of course, we again get a compilation error. Let's go ahead and add all the needed fields and constructor parameters into the NumberTriviaRepositoryImpl.
number_trivia_repository_impl.dart
import 'package:dartz/dartz.dart';
import 'package:meta/meta.dart';
import '../../../../core/error/failure.dart';
import '../../../../core/platform/network_info.dart';
import '../../domain/entities/number_trivia.dart';
import '../../domain/repositories/number_trivia_repository.dart';
import '../datasources/number_trivia_local_data_source.dart';
import '../datasources/number_trivia_remote_data_source.dart';
class NumberTriviaRepositoryImpl implements NumberTriviaRepository {
final NumberTriviaRemoteDataSource remoteDataSource;
final NumberTriviaLocalDataSource localDataSource;
final NetworkInfo networkInfo;
NumberTriviaRepositoryImpl({
@required this.remoteDataSource,
@required this.localDataSource,
@required this.networkInfo,
});
@override
Future<Either<Failure, NumberTrivia>> getConcreteNumberTrivia(int number) {
// TODO: implement getConcreteNumberTrivia
return null;
}
@override
Future<Either<Failure, NumberTrivia>> getRandomNumberTrivia() {
// TODO: implement getRandomNumberTrivia
return null;
}
}
What's Next
There's already quite a lot to digest from this part. We've created 3 contracts for the Repository's dependencies. Because we always implement things "from the inner parts out", the next part will be all about making the Repository implementation do its job. We will do this in TDD style, of course. Subscribe below and join Flutter developers growing their skills!
Is there any chance that you can push the completed project to github? Perhaps to a branch if you want to keep master inline with releases of these blog posts.
Thanks for the articles.. They were quite interesting.
My suggestion: Would be cool if you could make either vs-code extension or some CLI tool (for code and file generation) to assist developers.
What exactly do you have on your mind?
Seems reasonable and it would also be a good exercise for me as I’ve never written a VS Code extension nor a CLI tool ever ?
Please Reso Coder, how can use chopper with this architecture ?
Thank
I don’t really understand why we need to create these contracts in data sources, explain it to me please ?
its for testing purposes
Why, in the case of Repositories, don’t you use the Either to call the data? I thought it was good practice to handle exceptions as possible to where they might occur?
Can you be more specific about the content of your article? After reading it, I still have some doubts. Hope you can help me.