Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 3

Modeling Application Features

Recommended resources

Welcome back! In our previous lesson, we established the abstract, reusable foundations for our architecture by creating the base Failure and UseCase classes. This provided the essential scaffolding for consistent error handling and business logic execution.

Today, we will take the next logical step and apply these building blocks to a concrete feature. You will learn to define domain entities and use cases for a single application feature. This is where the abstract theory of Clean Architecture begins to take shape as the core logic of your application. We will see how to model the data your app cares about and how to define the specific actions it can perform, all within the isolated and independent domain layer.

1. The Domain Layer in Focus

Let's quickly refresh our memory on the role of the domain layer. It is the very heart of the application, containing the business rules and logic. Crucially, it has no dependencies on the presentation (UI) or data (API, database) layers. This makes it highly testable and reusable.

The two main components we will build in this layer are Entities and Use Cases.

This diagram shows the relationship between the layers. Notice that Entities and Use Cases are central to the Domain Layer. The Presentation Layer calls Use Cases, which in turn use Repositories to get data and construct Entities.
  • Entities: These are the core business objects of your application. Think of them as plain Dart classes that represent concepts like a User, a Product, or an Order. They contain no logic related to fetching or displaying data.
  • Use Cases: These classes represent a single, specific user action or business rule, such as LoginUser, GetProducts, or SubmitOrder. Each use case orchestrates the flow of data for its specific task by interacting with one or more repositories.

For our examples, we will build a simple "Number Trivia" feature, where a user can get a piece of trivia about a specific number or a random number.


2. Defining a Domain Entity

An entity is the simplest component. It's a data class that models a concept from your application's domain. The key is that it's completely independent of how or where the data comes from. Whether the data is fetched from a REST API as JSON or a local SQLite database, the Entity class remains the same.

Let's see how to define the NumberTrivia entity for our feature.

Flutter TDD Clean Architecture Course [2] – Entities & Use Cases

In this video from Reso Coder's "Flutter TDD Clean Architecture Course," the presenter demonstrates how to create the NumberTrivia entity.

Watch the section from creating the entity. Pay close attention to these key steps: How the entity is modeled based on the expected data (a JSON response), but only includes the fields the application actually needs. The use of the equatable package to enable value-based equality, which is a professional practice that simplifies testing and state management logic later on.

As you saw, the process is straightforward. We create a class that holds the data we need and nothing more. Using equatable is a best practice that helps you avoid bugs when comparing instances of your objects. Two NumberTrivia objects with the same text and number will be considered equal, which is exactly the behavior you want.

Here is the final code for our entity:

// lib/features/number_trivia/domain/entities/number_trivia.dart

import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';

class NumberTrivia extends Equatable {
  final String text;
  final int number;

  const NumberTrivia({
    required this.text,
    required this.number,
  });

  @override
  List<Object> get props => [text, number];
}

3. Defining a Use Case

With our entity defined, we can now create the business logic that operates on it. A use case class encapsulates a single, discrete action. For our first action, we'll implement GetConcreteNumberTrivia.

This use case will:

  1. Implement the abstract UseCase class we created in the last lesson.
  2. Depend on a repository contract (an abstract class) to fetch the data. This is a key part of the Dependency Rule. The use case doesn't know or care how the data is fetched, only that there is a repository that can fulfill its request.
  3. Expose a call method that takes the necessary parameters and returns a Future<Either<Failure, NumberTrivia>>.

Flutter TDD Clean Architecture Course [2] – Entities & Use Cases

Let's return to the same video to see how the use case is constructed. The presenter uses Test-Driven Development (TDD), writing a test first, which is a valuable mid-level skill. Even if you don't write tests first, the process reveals the final structure of the use case.

First, watch the brief introduction to use cases from what are use cases to understand their role. Then, watch the main implementation section from creating the use case. Focus on the final implementation of the GetConcreteNumberTrivia class and how it depends on the repository.

The use case acts as a simple orchestrator. It takes input, calls a method on its dependency (the repository), and returns the result. All the complex logic of fetching data, parsing it, and handling errors is delegated to lower layers, keeping our domain logic clean and focused.


4. Handling Multiple Use Cases and Parameter Types

Most features involve more than one action. To reinforce the pattern, let's define a second use case: GetRandomNumberTrivia. This provides a perfect opportunity to see how our generic UseCase base class handles actions that don't require any input parameters.

We will use the NoParams class we defined in the last lesson. This maintains a consistent structure across all our use cases, whether they need input or not.

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

This next video in the series shows how to refactor the domain layer and add the second use case.

First, watch how a dedicated Params class is created to encapsulate the input for GetConcreteNumberTrivia. This is a cleaner approach than passing primitive types directly. Watch from encapsulating parameters. Next, see how the GetRandomNumberTrivia use case is created, implementing UseCase<NumberTrivia, NoParams>. Watch from creating a second use case. Finally, notice the small but important refactor of moving the NoParams class to the core folder. This is a key principle of good project structure. Watch from sharing core components.

After these steps, our two use cases and their parameter classes look like this:

Use Case 1: With Parameters

// lib/features/number_trivia/domain/usecases/get_concrete_number_trivia.dart

class GetConcreteNumberTrivia implements UseCase<NumberTrivia, Params> {
  final NumberTriviaRepository repository;

  GetConcreteNumberTrivia(this.repository);

  @override
  Future<Either<Failure, NumberTrivia>> call(Params params) async {
    return await repository.getConcreteNumberTrivia(params.number);
  }
}

class Params extends Equatable {
  final int number;

  const Params({required this.number});

  @override
  List<Object> get props => [number];
}

Use Case 2: Without Parameters

// lib/features/number_trivia/domain/usecases/get_random_number_trivia.dart

class GetRandomNumberTrivia implements UseCase<NumberTrivia, NoParams> {
  final NumberTriviaRepository repository;

  GetRandomNumberTrivia(this.repository);

  @override
  Future<Either<Failure, NumberTrivia>> call(NoParams params) async {
    return await repository.getRandomNumberTrivia();
  }
}

// Located in lib/core/usecases/usecase.dart
class NoParams extends Equatable {
    @override
    List<Object?> get props => [];
}

You now have a complete, well-structured, and testable set of business rules for the "Number Trivia" feature.


Conclusion

In this lesson, you applied the abstract principles from our previous session to build the concrete domain layer for a feature. You have successfully defined what your application is and what it does, completely separate from any UI or data-sourcing concerns.

Key Takeaways:

  • Entities are simple, pure Dart classes that model your core business objects (e.g., NumberTrivia). They use equatable for reliable value-based comparison.
  • Use Cases are classes that encapsulate a single piece of business logic (e.g., GetConcreteNumberTrivia). They implement your abstract UseCase base class.
  • Use cases depend on repository interfaces (contracts), not concrete implementations, adhering to the Dependency Inversion Principle.
  • Use case parameters are encapsulated in dedicated Params classes for clarity, and the shared NoParams class is used for actions that require no input.

We have now defined our entities and created use cases that declare a need for a NumberTriviaRepository. In our next lesson, we will formally define that contract by creating the repository interface (abstract class) in the domain layer, solidifying the boundary between the domain and data layers.