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

28  comments

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!

About the author 

Matt Rešetár

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

You may also like

Flutter UI Testing with Patrol

Flutter UI Testing with Patrol
  • 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?

  • 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);
    }

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

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

  • Since its confusing when import Params of every usecase (assume we have a lot of usecase).
    I name it Params[UseCaseName],
    e.g: ParamsGetConcreteNumberTrivia

  • 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));
    });
    });
    }

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