Language: English | Português
A simple and extensible architecture for handling results in Flutter applications, based on the Result concept (success or failure).
Explore the documentation »
- About the Project
- The Problem
- Objectives
- Core Concepts
- Result Handling
- Error Handling Strategy
- License
- Author
This project demonstrates a clear and predictable approach to result management in Flutter applications, using an abstraction based on Result, adapted to Flutter’s ecosystem and the extensive use of asynchronous operations and UI feedback.
The focus of the architecture is not only to reduce code, but to make success and failure flows explicit, predictable, easy to understand, and simple to maintain, even in medium- and large-scale applications.
In any application, it is common to deal with operations that may fail.
Without a standardized approach, code often becomes repetitive, difficult to understand and maintain, and inconsistent in error handling.
Common issues include:
- Excessive use of
try/catch - Inconsistent error handling
- Unpredictable execution flows
- Constant null checks
- Difficulty maintaining and scaling larger applications
The lack of standardization makes code harder to read and increases the risk of unhandled errors or unexpected behavior.
The architecture aims to solve these problems by:
- Standardizing asynchronous operation results
- Avoiding excessive use of
try/catch - Making error flows predictable and controllable
- Facilitating local handling of specific errors
- Improving readability and maintainability
This approach does not replace internal exceptions but provides a clear semantic layer for managing results in the application.
Result<T> represents the outcome of an operation and can assume two states:
SuccessResult<T>: operation completed successfullyFailureResult<T>: operation failed
This separation removes the need to check for null values or catch exceptions at the point of use.
Failure represents known, expected, and treatable failures in the application.
It forms the foundation of the domain error model and must not be instantiated directly.
Any error the application recognizes and knows how to handle should be modeled as a subtype of Failure.
Failure is defined as a sealed class, ensuring all possible failures are explicitly controlled by the application domain.
sealed class Failure {}This approach provides:
- Exhaustiveness guarantees in failure handling
- Greater predictability of error flows
- Clear and explicit identification of failure reasons
Each failure must be represented by a concrete class extending Failure.
These classes can include:
- Error type (
FailureType) - User-friendly message
- Additional data for logs or metrics
Example:
final class ApiFailure extends Failure {
final FailureType type;
final String message;
ApiFailure({
required this.type,
required this.message,
});
}A new Failure implementation should be created when:
- The error is known and expected
- There is a clear action associated with the error
- The error is part of the application domain rules
Common examples:
- API Failure (
ApiFailure) - Validation Failure (
ValidationFailure) - Cache Failure (
CacheFailure) - Authentication Failure (
AuthFailure) - Parsing Failure (
ParsingFailure)
Purpose of the model:
- Centralize and standardize failure handling
- Prioritize clarity and predictability
The API provides different methods depending on the need:
- Returning a value from the result
- Executing side effects
The central distinction is:
foldandfoldAsyncreturn a valuewhenandwhenAsyncdo not return a value
Synchronous and asynchronous versions differ only in whether the callback requires await.
Executes synchronous callbacks and returns a value.
final value = result.fold(
onSuccess: (data) => data,
onFailure: (failure) => null,
);Executes asynchronous callbacks and returns a Future<T>.
final value = await result.foldAsync(
onSuccess: (data) async => data,
onFailure: (failure) async => null,
);Used when the goal is to perform actions without producing a value.
Examples:
- State updates
- Showing messages
- Navigation
- Additional commands
Executes synchronous callbacks without returning a value.
result.when(
onSuccess: (value) {
// State update
},
onFailure: (failure) {
// Synchronous handling
},
);Executes asynchronous callbacks and returns Future<void>.
await result.whenAsync(
onFailure: (failure) async {
// Dialog, navigation, or other side effects
},
);The architecture supports two levels of failure handling: local and global.
Local handling should be used when the current context has a specific response to a failure. It allows corrective action to be taken directly where the error occurs.
await result.whenAsync(
onFailure: (failure) async {
switch (failure) {
case ApiFailure():
// Specific local handling
break;
}
},
);Local handling enables:
- Screen state updates
- Custom messages or dialogs
- Redirection or retry of operations
Global handling acts as a fallback for failures not handled locally or unknown. It ensures any unhandled error receives a consistent response, maintaining predictability and standardization in the UI.
The global handler logic:
- Receives failures not handled locally
- Identifies the type of failure
- Converts the failure into a user-friendly message
- Shows standard visual feedback (dialog, alert, snackbar)
- Avoids business logic decisions; domain remains isolated
Example:
Future<void> handleError(BuildContext context, Failure failure) async {
await showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Error'),
content: Text(failure.message),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Ok'),
),
],
);
},
);
}Usage in a result flow:
await result.whenAsync(
onFailure: (failure) async {
switch (failure) {
case ApiFailure():
// Local handling
break;
default:
await handleError(context, failure);
}
},
);This flow ensures:
- Failures are checked in the local context first
- Specific rules are applied when defined
- Unhandled failures are delegated to the global handler
- The global handler provides consistent feedback without duplicating logic
Distributed under the MIT License. See the LICENSE file for details.
Developed by Dário Matias:
- Portfolio: https://dariomatias-dev.com
- GitHub: https://github.com/dariomatias-dev
- Email: matiasdario75@gmail.com
- Instagram: https://instagram.com/dariomatias_dev
- LinkedIn: https://linkedin.com/in/dariomatias-dev