5

Flutter TDD Clean Architecture Course [3] – Domain Layer Refactoring

Our Number Trivia App is moving along nicely. In the previous part, we created an EntityRepository 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.

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!

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!

Matej Rešetár
 

Matej is an app developer with a knack for teaching others. If he's not programming, making tutorials or doing other business, he's mostly working out, listening to audiobooks and taking cold showers.

  • Abhishek Abhishek says:

    Please update code in github. It doesn’t have the codes from tutorial

  • EwertonRP says:

    Hi Matej. When do you plan to release upcoming parts?

    PS: google login is broken.

  • >