Skip to main content

Welcome to a Grasp lesson

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

Lesson 2

Building Core Architectural Components

Recommended resources

Hello! In our last lesson, we established a strong theoretical foundation for Clean Architecture, covering the three main layers and the all-important Dependency Rule. Now, we'll start translating that theory into tangible code.

Today's lesson focuses on creating the reusable building blocks that will form the backbone of our application's architecture. You will learn to define and implement the core shared components of the architecture, such as base UseCase and Failure classes. By creating these generic, abstract components first, we ensure that every feature we build later will be consistent, predictable, and robust.

1. The Need for Structured Error Handling

In any real-world application, things can go wrong: a network request might fail, a server might return an error, or data might be corrupted. A junior approach might be to wrap code in a try-catch block and show the raw error message to the user. This leads to a poor user experience and can expose internal implementation details.

A professional, mid-level approach involves defining a clear, consistent strategy for handling errors. Let's start by watching a segment that explains the pitfalls of unstructured error handling.

Proper Error Handling in Flutter & Dart (#1 - Principles)

In this video, "Proper Error Handling in Flutter & Dart," Matt from Reso Coder explains why simply catching exceptions and displaying their messages is a bad practice.

Watch the section from the problem with exceptions. Pay close attention to his arguments against showing low-level error messages (like SocketException) directly in the UI.

As the video demonstrates, we need a way to transform low-level, technical Exceptions into high-level, application-specific errors that the UI layer can easily understand and display. This is where the Failure class comes in.


2. Defining the Failure Class

A Failure is a simple class that represents a domain-specific error in our application. Instead of dealing with dozens of potential Exception types from various packages, the rest of our app will only have to understand a handful of Failure types that we define.

The Data layer becomes responsible for catching technical Exceptions (e.g., DioException, SocketException) and mapping them to one of our Failure classes.

Let's see how to create a base Failure class.

Rules for Clean Architecture - Cursor Directory

This document provides a concise set of rules and code examples for Clean Architecture. We'll focus on its definition of Failure classes.

Read the sections titled Error Handling and Dartz Error Handling. Then, review the code example that starts with // Define base failure class. Notice how a generic Failure class is defined, and then more specific failures like ServerFailure and CacheFailure extend it.

The key idea is to have an abstract base class Failure, which ensures all our failures have a consistent structure (e.g., they must have a message).

// lib/core/error/failures.dart

import 'package:equatable/equatable.dart';

abstract class Failure extends Equatable {
  final String message;

  const Failure(this.message);

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

// Specific Implementations
class ServerFailure extends Failure {
  const ServerFailure(super.message);
}

class CacheFailure extends Failure {
  const CacheFailure(super.message);
}

class NetworkFailure extends Failure {
  const NetworkFailure(super.message);
}

Now, the Data layer can do its job: catch a DioException and return a Left(ServerFailure('Could not connect to the server.')), and the UI layer only ever sees the clean ServerFailure object.


3. The UseCase Abstract Class

Just as we standardized error handling, we need to standardize how we execute business logic. In Clean Architecture, each specific user action or business rule is encapsulated in its own class, called a UseCase. For example: LoginUser, GetWeatherForCity, UpdateUserProfile.

To ensure all use cases have a consistent interface, we create a generic, abstract UseCase class.

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

This video from Reso Coder, "Flutter TDD Clean Architecture Course [3] – Domain Layer Refactoring," is excellent for understanding how to structure the domain layer. We'll start with the base UseCase class.

Watch the segment from creating the base class. Focus on: The reason for creating an abstract base class (to enforce a predictable interface). The use of generic types: Type for the return value and Params for the input parameters.

A small but elegant improvement you can make is to name the execution method call. This turns your UseCase class into a "callable" class in Dart, meaning you can execute it as if it were a function.

// Instead of this:
// final result = await getUserUseCase.execute(params);

// You can do this:
final result = await getUserUseCase(params);

The video touches on this refactor right at the beginning. It's a common convention in professional Flutter projects.


4. Connecting UseCase and Failure with Either

Now we have a standard way to represent success (Type) and failure (Failure). How do we make a UseCase return one or the other? We use a functional programming concept called Either.

The Either type, from packages like fpdart or dartz, can hold one of two values: a Left value or a Right value. By convention:

  • Left is used for the failure case (our Failure object).
  • Right is used for the success case (our Type object).

This allows our UseCase's call method to have a single, predictable return type that handles both scenarios.

Flutter Clean Architecture - Dhemas Nurjaya

This article by Dhemas Nurjaya provides a great, clear implementation of the base UseCase that incorporates Either.

Read the section Core - Domain. Pay close attention to the signature of the call method: Future<Either<Failure, Type>> call(Params params);. This signature is the heart of our architecture's data flow. It clearly states: "This is an asynchronous operation that will return either a Failure on the left or the expected Type on the right."

So, our final abstract UseCase looks like this:

// lib/core/usecases/usecase.dart

import 'package:fpdart/fpdart.dart';
import 'package:equatable/equatable.dart';
import '../error/failures.dart';

abstract class UseCase<Type, Params> {
  Future<Either<Failure, Type>> call(Params params);
}

5. Handling Use Case Parameters

The final piece of the puzzle is the Params generic type. Some use cases require input (e.g., GetUser needs a userId), while others don't (e.g., GetRandomQuote). We need a standardized way to handle both.

The solution is to create small, dedicated classes for parameters.

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

Let's return to the Reso Coder video to see how to handle parameters in practice.

Watch these two final segments: First, see how to create a dedicated Params class for a use case that requires input. Encapsulating parameters. This keeps the call method signature clean and makes the code more readable and extensible. Next, see the pattern for use cases that don't require any parameters. Handling no parameters. Here, we create a simple NoParams class to act as a placeholder.

By creating a NoParams class, we can satisfy the generic constraint of our base UseCase and maintain a consistent structure across the entire application.

// lib/core/usecases/usecase.dart (add this to the same file)

class NoParams extends Equatable {
  @override
  List<Object?> get props => [];
}

Now, a use case that gets a random entity would be defined as class GetRandomEntity implements UseCase<Entity, NoParams>.


Conclusion

In this lesson, you've moved from theory to practice by defining the fundamental, shared components of a Clean Architecture. These abstract classes are the essential scaffolding upon which all your application's features will be built.

Key Takeaways:

  • A Failure class is used to transform low-level Exceptions into clean, domain-specific errors that the application can easily handle.
  • An abstract UseCase class with a call method enforces a consistent contract for executing all business logic operations.
  • The return type Future<Either<Failure, Type>> elegantly handles both success and failure scenarios for any operation.
  • Use case parameters are encapsulated in dedicated Params classes for clarity and maintainability, with a NoParams class used as a placeholder for use cases that require no input.

In our next lesson, we will put these building blocks to work. You will define domain entities and use cases for a single application feature, taking the first concrete step in building out the Domain layer of our project.