Flutter TDD Clean Architecture Course [5] – Contracts of Data Sources

7  comments

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.

TDD Clean Architecture Course
This post is just one part of a tutorial series. See all of the other parts here and learn to architect your Flutter apps!

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.

Contract allows for the independence of the domain layer

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...

Location of the file

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.

The place for "core" exceptions

exception.dart

class ServerException implements Exception {}

class CacheException implements Exception {}
We're keeping it simple by not having any fields inside the custom Exceptions. If you'd like to convey more information about the error, feel free to add a message field into the class in your projects.

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);
}
Notice that in neither of the Data Sources had we written code which seems to be dependent on some outer layer of the app.

The URL for the API will have to be a String, yet, the type of the number parameter for the remote Data Source is an integer...
Future<NumberTriviaModel> getConcreteNumberTrivia(int number);

This coding practice allows you to swap the low-level http package for something like chopper without any significant issues.

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.

Location of the test 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!

About the author 

Matt Rešetár

Matt is an app developer with a knack for teaching others. Working as a freelancer and most importantly developer educator, he is set on helping other people succeed in their Flutter app development career.

You may also like

  • 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.

  • I don’t really understand why we need to create these contracts in data sources, explain it to me please ?

  • 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?

  • {"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}
    >