The Domain Layer

Entity, Use Case and the interactions with Outputs and Inputs#

Let's start by exploring the Domain Layer, the heart of anything important for the project.

Domain Layers

Entity#

Let's begin with the Entities. If you are familiar with Domain Driven Design (DDD), you already know how important are the Domain components to an app. When the design is robust, there is a zero chance that the state of the app failes due to validation or null errors. Domain models have strict rules so it is very hard to create instances with inconsistent states.

The sum of all your Entities is the state of the whole feature. This state will be kept alive as long as its Use Case exists. Since we create it when the app is executed (using a provider), this reference is alive until the app is removed from memory.

So it is important to understand that this state needs initial values and rules governing how those values change. When writing an Entity, try to follow these rules:

  1. Entities don't depend on other files or libraries except for the clean framework import. This is the most central layer, so it should not need anything, not even from other features. Shared enums are even problematic, since feature requirements could change, forcing you to refactor the affected features.

  2. Attributes should be final and have initial values on construction. Some of them could be required values, inserted at the time the UseCase is created as well (explained in the following section).

  3. Use proper data types instead of relaying on parsers. For example, use DateTime instead of a String for a date attribute. You can parse the date in Presenters and Gateways.

  4. It is OK to create a hierarchy of entities, but keep a single ancestor that the Use Case can create easily. Composition is much better than inheritance. Functional constructs like Either and Unions are useful here as well.

  5. Add generators like copyWith to create instances based on current values. This simplifies the Use Case code.

It is OK to add methods to validate the consistency of the data. For example:


class AccountEntity extends Entity {
  AccountEntity({this.isRegistered = false, this.userName});

  final bool isRegistered;
  final UserNameEntity? userName;
}

class UserNameEntity extends Entity{
  UserNameEntity({required this.firstName, required this.lastName})
    : assert(firstName.isNotEmpty && lastName.isNotEmpty);

  final String firstName;
  final String lastName;

  String get fullName => '$firstName $lastName';
}

See how it is virtually impossible to create an inconsistent user name with null or empty first and last name, and we have a dynamic getter that builds the full name.

This has two main advantages:

  1. Developers will not write wrong code around this entity fields, since they have syntax errors or exceptions that are easy to catch while writing tests and coding.

  2. The custom logic to compose fields is delegated to the Entity and is not floating around next other business logic from the Use Case, making the Use Case code easier to read.

Try to delegate similar helper methods to the Entity, where they only rely on the data, such as form validations, math calculations, derivatives, etc.

Use Case#

Use Cases live outside the Entities, on its own layer. Use Cases will create and manipulate Entities internally, while transferring data from Inputs and into Outputs. Lets look at one simple example to understand the class:

class MyUseCase extends UseCase<MyEntity> {
  MyUseCase()
      : super(
          entity: MyEntity(),
          transformers: [
            OutputTransformer.from(
              (entity) => MyUIOutput(data: entity.data),
            ),
            InputTransformer<MyEntity, MyInput>.from(
              (entity, input) => entity.copyWith(data: input.data),
            ),
          ],
        );
}

A typical Use Case will need to create an Entity. The output filters attribute lets you set up a list of possible "channels" that Presenters can use to subscribe to.

Here, MyUseCase has only one output, so the Presenter only needs to listen to MyUIOutput instances, which will be generated when the Presenter is created and any time the Entity data field changes.

Notice that the filter is a Map of the type of the Output and a function that receives the current Entity instance. It is intended to do it this way so its easier to isolate the code and help the developer think on simple terms and avoid having complex method calls.

Outputs are meant to only hold a subset of the data available in the Entity, and the way the Presenter and UseCase communication works internally, a new Output is only generated if the fields used for its construction change. In this example, the Use Case can alter the Entity, but if the data field remains the same, no new Output is created.

Input filters work in a similar way. If a Gateway is attached to a Use Case, it produces a specific type of Input. This class allows a Gateway to send a MyInput instance, which will be used by the input filter anonymous method to create a new version of the Entity based on the data received.

So this means that a MyInput instance is received, it will trigger a Entity change on the data field, and thus generate a new MyUIOutput.

Entities can be changed at any time in other methods inside the Use Case, as in here:

  // Method inside the Use Case

  void updateAmount(double newAmount){
    if (entity.isAmountValid(newAmount)) {
      entity = entity.copyWith(amount: newAmount);
    } else {
      entity = entity.copyWith(error: Errors.invalidAmount);
    }
  }

The entity attribute is available in any UseCase. Each time we need to change at least one field, we need to replace the whole instance. If this is not done, the Use Case will not generate any Output, since it behaves like a ValueNotifier

Outputs for Presenters and Gateways#

Use Cases have no knowledge of the world of the outside layers. They only create Outputs that can be listened by anything. That is why you have to keep the implementation independent from any assumption about the data.

For example, an Output can contain data that will be stored in a database, visualized on a screen, or sent to a service. Only the external layers will determine where the data goes and how it is used.

There are two ways the Use Case sends out Outputs. We already reviewed the output filters, which generate them after the entity changes.

But to create outputs on demand and wait for some kind of response from the outside layers, we use the following:

  void fetchUserData(){
    await request(
      FetchUserDataGatewayOutput(),
      onSuccess: (UserDataInput input) {
        return entity.copyWith(name: input.name);
      },
      onFailure: (_) {
        return entity.copyWith(error: Error.dataFetchError);
      },
    );
  }

The request method creates a Future where the instance of FetchUserDataGatewayOutput is published. If no one is listening to this specific type of output, an error is thrown. During development you might attach dummy Gateways to help you complete the Use Case behavior without the need to write any outside code.

The request has two callbacks, for success and failures respectively.

Notice how the onSuccess callback is receiving an Input. Remember UseCase communicates with the external layer only with Inputs and Outputs. When outside data needs to come inside the class, it has to be through an Input.

We have already done the Presenter implementation, and now you have a bit more understanding on how it connects to the Use Case. As long as you plan correctly which Outputs will be used on the output filter and by the Presenter, then everything will be handled internally.

Gateways connections will be explained on the next section of the Codelab.

Inputs for Presenters and Gateways#

When Gateways and Presenters need to send Inputs to the Use Case, both can use this method:

  useCase.setInput<MyInput>(MyInput('foo'));

Gateways do this for you internally, but Presenters are free to use this method at anytime instead of calling a specific method on the UseCase.

Coding Use Cases#

We will start implementing the Use Case now.

lib/features/home/domain/home_entity.dart#

import 'package:clean_framework/clean_framework.dart';

enum HomeStatus { initial, loading, loaded, failed }

class HomeEntity extends Entity {
  HomeEntity({
    this.pokemons = const [],
    this.pokemonNameQuery = '',
    this.status = HomeStatus.initial,
    this.isRefresh = false,
  });

  final List<PokemonData> pokemons;
  final String pokemonNameQuery;
  final HomeStatus status;
  final bool isRefresh;

  @override
  List<Object?> get props {
    return [pokemons, pokemonNameQuery, status, isRefresh];
  }

  @override
  HomeEntity copyWith({
    List<PokemonData>? pokemons,
    String? pokemonNameQuery,
    HomeStatus? status,
    bool? isRefresh,
  }) {
    return HomeEntity(
      pokemons: pokemons ?? this.pokemons,
      pokemonNameQuery: pokemonNameQuery ?? this.pokemonNameQuery,
      status: status ?? this.status,
      isRefresh: isRefresh ?? this.isRefresh,
    );
  }
}

class PokemonData extends Entity {
  PokemonData({
    required this.name,
    required this.imageUrl,
  });

  final String name;
  final String imageUrl;

  @override
  List<Object?> get props => [name, imageUrl];
}

The next step will be to create the Use Case. Create a new file called home_use_case.dart inside the domain directory.

lib/features/home/domain/home_use_case.dart#

import 'package:clean_framework/clean_framework.dart';

import 'home_entity.dart';

class HomeUseCase extends UseCase<HomeEntity> {
  HomeUseCase() : super(entity: HomeEntity());

  Future<void> fetchPokemons({bool isRefresh = false}) async {
    if (!isRefresh) {
      entity = entity.copyWith(status: HomeStatus.loading);
    }

    // TODO: Make a request to fetch the pokemons

    if (isRefresh) {
      entity = entity.copyWith(isRefresh: false, status: HomeStatus.loaded);
    }
  }
}

After creating the Use Case, we need to create an UI Output. This will be used by the Presenter later to display the data on the screen. Create a new file called home_ui_output.dart inside the domain directory.

lib/features/home/domain/home_ui_output.dart#

import 'package:clean_framework/clean_framework.dart';

import 'home_entity.dart';

class HomeUIOutput extends Output {
  HomeUIOutput({
    required this.pokemons,
    required this.status,
    required this.isRefresh,
  });

  final List<PokemonData> pokemons;
  final HomeStatus status;
  final bool isRefresh;

  @override
  List<Object?> get props => [pokemons, status, isRefresh];
}

Now we need to create an output transformer so that the raw data in Use Case(i.e. Entity) can be transformed into UI Output. Create the following class in the Use Case.

class HomeUIOutputTransformer extends OutputTransformer<HomeEntity, HomeUIOutput> {
  @override
  HomeUIOutput transform(HomeEntity entity) {
    final filteredPokemons = entity.pokemons.where(
      (pokemon) {
        final pokeName = pokemon.name.toLowerCase();
        return pokeName.contains(entity.pokemonNameQuery.toLowerCase());
      },
    );

    return HomeUIOutput(
      pokemons: filteredPokemons.toList(growable: false),
      status: entity.status,
      isRefresh: entity.isRefresh,
    );
  }
}

And since we need to take search input from the UI as well to filter the pokemons, we need to create an input & input transformer as well. Add the following classes to the file.

class PokemonSearchInput extends Input {
  PokemonSearchInput({required this.name});

  final String name;
}

class PokemonSearchInputTransformer extends InputTransformer<HomeEntity, PokemonSearchInput> {
  @override
  HomeEntity transform(HomeEntity entity, PokemonSearchInput input) {
    return entity.copyWith(pokemonNameQuery: input.name);
  }
}

Finally these transformers need to be added to the Use Case.

class HomeUseCase extends UseCase<HomeEntity> {
  HomeUseCase()
      : super(
          entity: HomeEntity(),
          transformers: [
            HomeUIOutputTransformer(),
            PokemonSearchInputTransformer(),
          ],
        );

  ...
}

Congratulations if you made it until this point, on the next section we will plug-in gateway to the domain.