Our Number Trivia App is moving along nicely. In the previous part, we created an Entity, Repository contract, and the first Use Case - GetConcreteNumberTrivia using test driven development. Today, we're going to add another Use Case which will uncover a nice opportunity for refactoring the code.
Callable Classes
Did you know that in Dart, a method named call can be run both by calling object.call() but also by object()? That's the perfect method to use in the Use Cases! After all, their class names are already verbs like GetConcreteNumberTrivia, so using them as "fake methods" fits perfectly.
In the spirit of TDD, we'll first modify the test (get_concrete_number_test.dart) to no longer call the execute method:
final result = await usecase(number: tNumber);
And since the code doesn't even compile, we can modify the GetConcreteNumberTrivia class immediately:
Future<Either<Failure, NumberTrivia>> call({ ...
The test code should now pass.
Adding Another Use Case
In addition to getting trivia for a concrete number, our app will also get trivia for a random number. This means we need another use case - GetRandomNumberTrivia. The Numbers API we're using has actually a different API endpoint for concrete and random numbers, so we won't generate the number ourselves. Otherwise, the number generation code would be executed inside the domain layer precisely in the GetRandomNumberTrivia use case. Generating numbers is a business logic, after all.
UseCase Base Class
As clean coders, we surely like when our code has a predictable interface. Public methods and properties of classes which do basically the same thing should have standardized names.
When it comes to Use Cases, every single one of them should have a call method. It doesn't matter if the logic inside the Use Case gets us a NumberTrivia or sends a space shuttle to the Moon, the interface should be the same to prevent any confusion.
One way to enforce a stable interface of a class is to rely on the implicit "word of the programmer". Sadly, programmers aren't well known for remembering things. Heck, I even watch some of my old tutorials because I've already forgotten how to do certain things which I've previously known!
Another way to prevent one class having a call method and another an execute method is to provide an explicit interface (in Dart's case an abstract class), which is unforgettable to derive from. Like a UseCase base class, for example.
The following code will go into core/usecases, since this class can be shared across multiple features of an app. And of course, there is no point in testing an abstract class, so we can write it straight away.
usecase.dart
import 'package:dartz/dartz.dart';
import 'package:equatable/equatable.dart';
import '../error/failure.dart';
// Parameters have to be put into a container object so that they can be
// included in this abstract base class method definition.
abstract class UseCase<Type, Params> {
Future<Either<Failure, Type>> call(Params params);
}
// This will be used by the code calling the use case whenever the use case
// doesn't accept any parameters.
class NoParams extends Equatable {}
Extending the Base Class
As you can see, we added two type parameters to the UseCase class. One is for the "no-error" return type, which in the case of our app will be the NumberTrivia entity. The other type parameter, Params, is going to cause some minor code changes in the already present GetConcreteNumberTrivia use case.
Every UseCase extending class will define the parameters which get passed into the call method as a separate class inside the same file. While we're at it, let's also extend the UseCase. Other changes dealing with the workings of the class will come only after updating the test - we're doing TDD!
get_concrete_number_trivia.dart
class GetConcreteNumberTrivia extends UseCase<NumberTrivia, Params> {
...
}
class Params extends Equatable {
final int number;
Params({@required this.number}) : super([number]);
}
Now we know that the call has to take in a Params object, instead of having the integer be the parameter directly. So, because we wrote a test in the previous part, we can use it to be confident in our code. All we have to do is:
- Update the test to use Params.
- It won't compile.
get_concrete_number_trivia_test.dart
...
test(
'should get trivia for the number from the repository',
() async {
// arrange
when(mockNumberTriviaRepository.getConcreteNumberTrivia(any))
.thenAnswer((_) async => Right(tNumberTrivia));
// act
final result = await usecase(Params(number: tNumber));
// assert
expect(result, Right(tNumberTrivia));
verify(mockNumberTriviaRepository.getConcreteNumberTrivia(tNumber));
verifyNoMoreInteractions(mockNumberTriviaRepository);
},
);
- Update the production code. Use Params as the parameter for the call method.
- Run the test - it should pass, code confidence goes through the roof.
get_concrete_number_trivia.dart
class GetConcreteNumberTrivia extends UseCase<NumberTrivia, Params> {
...
@override
Future<Either<Failure, NumberTrivia>> call(Params params) async {
return await repository.getConcreteNumberTrivia(params.number);
}
}
...
GetRandomNumberTrivia
Adding this new use case is now very simple - we've agreed upon a an interface which each UseCase must have. Also, because of the simple nature of the Number Trivia App, this new use case will only get data from the repository.
We again start by writing the test - create a new file in the "test/.../usecases" folder. Most of the code is copied from the test for the previous use case.
get_random_number_trivia_test.dart
import 'package:clean_architecture_tdd_prep/core/usecase/usecase.dart';
import 'package:clean_architecture_tdd_prep/features/number_trivia/domain/entities/number_trivia.dart';
import 'package:clean_architecture_tdd_prep/features/number_trivia/domain/repositories/number_trivia_repository.dart';
import 'package:clean_architecture_tdd_prep/features/number_trivia/domain/usecases/get_random_number_trivia.dart';
import 'package:dartz/dartz.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
class MockNumberTriviaRepository extends Mock
implements NumberTriviaRepository {}
void main() {
GetRandomNumberTrivia usecase;
MockNumberTriviaRepository mockNumberTriviaRepository;
setUp(() {
mockNumberTriviaRepository = MockNumberTriviaRepository();
usecase = GetRandomNumberTrivia(mockNumberTriviaRepository);
});
final tNumberTrivia = NumberTrivia(number: 1, text: 'test');
test(
'should get trivia from the repository',
() async {
// arrange
when(mockNumberTriviaRepository.getRandomNumberTrivia())
.thenAnswer((_) async => Right(tNumberTrivia));
// act
// Since random number doesn't require any parameters, we pass in NoParams.
final result = await usecase(NoParams());
// assert
expect(result, Right(tNumberTrivia));
verify(mockNumberTriviaRepository.getRandomNumberTrivia());
verifyNoMoreInteractions(mockNumberTriviaRepository);
},
);
}
Of course, this test fails and the implementation of the GetRandomNumberTrivia is as follows:
get_random_number_trivia.dart
import 'package:dartz/dartz.dart';
import '../../../../core/error/failure.dart';
import '../../../../core/usecase/usecase.dart';
import '../entities/number_trivia.dart';
import '../repositories/number_trivia_repository.dart';
class GetRandomNumberTrivia extends UseCase<NumberTrivia, NoParams> {
final NumberTriviaRepository repository;
GetRandomNumberTrivia(this.repository);
@override
Future<Either<Failure, NumberTrivia>> call(NoParams params) async {
return await repository.getRandomNumberTrivia();
}
}
The test will now pass and with that, we've just fully implemented the domain layer of the Number Trivia App. In the next part, we will start working on the data layer containing the Repository implementation and Data Sources. Subscribe to below to receive emails about new tutorials and much more from the world of Flutter!
Please update code in github. It doesn’t have the codes from tutorial
I’ve just checked and the GitHub repository seems to contain all the code from this 3rd part.
Matej is updating the code step by step. We are now at 3rd part, so the github repository contains code from parts 1, 2 and 3. Let’s wait for the next tutorials.
Hi Matej. When do you plan to release upcoming parts?
PS: google login is broken.
The next one is coming out on Monday. Thanks for letting me know about the login. It’s probably because Google + no longer exists.
Hi Matej, really nice tutorial, thank you for this.
When i’m trying to extend UseCase abstract class with GetConcreteNumberTrivia, vscode is tells me that “Missing concrete implementation of UseCase.call.
Try implementing the missing method, or make the class abstract.” so i do have to override the call method or create the noSuchMethod method, do you have any idea why?
Hello!
You could try implementing the UseCase base class, instead of extending it.
Hi Matej, you implement the UseCase base class in the video tutorial but you extend it in the written one. I suppose the correct way is implementation, isn’t it ?
Hi Matej
Thank you for the tutorials.
I’m trying to adapt your tutorial code to get a list of number trivia from a (firestore) database. Does the following interface code even make sense? – to have Future and Stream. That way I can still use the abstract class usecase.dart for all my usecases. Or should I setup an alternative use case for streams – which will sort of tie my business logic to data retrieval implementation – as I might use a datasource that doesn’t have streaming? What are your thoughts – maybe this is an idea for a 15th episode??
abstract class NumberTriviaRepository {
Future<Either> getConcreteNumberTrivia(int number);
Future<Either> getRandomNumberTrivia();
Future<Either<Failure, Stream<List>>> getManyConcreteNumberTrivia(int count);
}
[…] aclarar que el contenido original es de Resocoder, lo que he hecho es una traducción al español del contenido. Al final de este artículo está […]
There are wrong link under the ‘next post’ button in this article – it’s https://resocoder.com/2019/09/05/persist-bloc-state-in-flutter-hydrated-bloc-tutorial/
instead of fourth chapter)
Thank you resoo coder
This tutorial has been fundamental for improve my knowledgements. Thank you Matt.
You’re welcome!
The tutorials are amazing! They are key to improve software developing skills in flutter or in general.
I believe it would be a bit clearer if I ‘d use Null instead of NoParams, and the “call” signature to be like this “call([Null _])”. It also succeed the tests with no parameters at all.
Why the need of Params? seems to me a redundant addition
Yea. Can someone explain how using Params helps?
Hi Matt, thanks for you work!
I’m want to implement this pattern were my entity class has subclasses. I’m trying to figure out how to extend the functionality of these subclasses into the Data>Model layer so that I can add the fromJson, toJson methods. Do you have any suggestions for working with nested Entities?
Hello Matt, love the content.
You have some UI bugs in the article, under ‘Callable Classes’.
The code blocks obscures some text.
I have a problem with the following statement.
“Every UseCase extending class will define the parameters which get passed into the call method as a separate class inside the same file.”
The problem is that there will be multiple definitions of the “params” class and on the bloc. dart, where we import a lot of use cases, will get confused on which “Params” class to use.
This app doesn’t have this problem since there is only one use case where the Params is required.
Do we have the design fix for this?
how about naming them _Params in each useCase dart file? so that it is private and cannot be shared to other files, so no chance of confusion?
Hi Matt, thanks for this amazing tutorial.
This is my doubt:
How many repositories do I need to create? I mean, if I work with 3 entities (e.g. Restaurants, Users and Reviews) is it better to have one repository for each of them or is it OK with one repository for all of them?
How did you create the `aaaTest` template in vsCode?
I’m guessing this is it: https://gist.github.com/arifikhsan/e221a8578f9b8adc03a568aec34d0893
🙂
Since its confusing when import Params of every usecase (assume we have a lot of usecase).
I name it Params[UseCaseName],
e.g: ParamsGetConcreteNumberTrivia
using this architecture app size increases a lot !
For the GetRandomNumberTrivia usecase we’ll use the same repository we mocked in the last class, which is, MockMockNumberTriviaRepository and, of course, import its file.
import ‘package:clean_architecture_tdd_course/core/usecases/usecase.dart’;
import ‘package:clean_architecture_tdd_course/features/number_trivia/domain/entities/number_trivia.dart’;
import ‘package:clean_architecture_tdd_course/features/number_trivia/domain/repositories/number_trivia_repository.dart’;
import ‘package:clean_architecture_tdd_course/features/number_trivia/domain/usecases/get_random_number_trivia.dart’;
import ‘package:dartz/dartz.dart’;
import ‘package:flutter_test/flutter_test.dart’;
import ‘package:mockito/annotations.dart’;
import ‘package:mockito/mockito.dart’;
import ‘get_concrete_number_trivia_test.mocks.dart’;
void main() {
late final GetRandomNumberTrivia usecase;
late final MockMockNumberTriviaRepository repository;
setUp(() {
repository = MockMockNumberTriviaRepository();
usecase = GetRandomNumberTrivia(repository);
});
final tNumberTrivia = NumberTrivia(text: ‘test’, number: 1);
group(‘Successful tests:’, () {
test(‘Should get trivia from the repository’, () async {
// Arrange
when(repository.getRandomNumberTriviaRepository())
.thenAnswer((_) async => Right(tNumberTrivia));
// Act
final result = await usecase(NoParams());
// Assert
verify(repository.getRandomNumberTriviaRepository());
verifyNoMoreInteractions(repository);
expect(result, Right(tNumberTrivia));
});
});
}