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
Failureobject). - Right is used for the success case (our
Typeobject).
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
Failureclass is used to transform low-levelExceptionsinto clean, domain-specific errors that the application can easily handle. - An abstract
UseCaseclass with acallmethod 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
Paramsclasses for clarity and maintainability, with aNoParamsclass 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.