bloc_error_control 1.1.2
bloc_error_control: ^1.1.2 copied to clipboard
Declarative error handling for BLoC with Zones and optional code generation
🛡️ Bloc Error Control #
An advanced error handling and event lifecycle management solution for Flutter applications built with BLoC. The package transforms chaotic asynchronous exceptions into structured states while preventing memory leaks and resource waste.
🎯 When to Use This Package #
✅ Use it if:
- You have a large project (50+ events)
- You're tired of try-catch boilerplate
- You need automatic request cancellation
- You're ready to learn about Zones
❌ Don't use it if:
- You have a small project (less than 10 events)
- You're not familiar with Zones
- You find it easier to write try-catch
🎯 Why do you need this? #
The Problem: Typical BLoC code #
on<LoadUser>((event, emit) async {
emit(UserLoading());
try {
final user = await userRepository.getUser(event.id);
emit(UserLoaded(user));
} on SocketException {
emit(UserError('No internet connection'));
} on ApiException catch (e) {
emit(UserError(e.message));
} catch (e, stack) {
logger.error('Unexpected', error: e, stackTrace: stack);
emit(UserError('Something went wrong'));
}
});
Problems:
- 80% of the code is error handling
- Easy to forget handling an exception
- No automatic cancellation of "hanging" requests
The Solution: Your code becomes clean
on<LoadUser>((event, emit) async {
emit(UserLoading());
final user = await userRepository.getUser(event.id, token: contextToken);
emit(UserLoaded(user));
});
That's it! No try-catch. Errors are handled centrally..
📦 Features #
| Feature | Description |
|---|---|
| 🚫 No try-catch | Event handlers contain only business logic |
| 🎯 Centralized error handling | mapErrorToState — one method for all errors |
| 🔌 Auto-cancellation of requests | contextToken automatically cancels requests on repeated events |
🧹 Cleanup on close() |
All active requests are cancelled when the bloc is closed |
| 📝 Flexible logging | Plug any logger (Sentry, Firebase, custom) |
| ⚡️ Works with any transformers | sequential(), restartable(), debounce() etc. |
| 🧬 Optional code generation | Minimal boilerplate through annotations |
🚀 Quick start #
class UserBloc extends Bloc<UserEvent, UserState>
with BlocErrorControlMixin<UserEvent, UserState> {
UserBloc() : super(UserInitial()) {
on<LoadUserEvent>(_onLoadUser);
}
@override
UserState? mapErrorToState(Object error, StackTrace stack, UserEvent event) {
// All errors from handlers go here
if (error is SocketException) {
return UserError('No internet connection');
}
return UserError('Something went wrong');
}
Future<void> _onLoadUser(LoadUserEvent event, Emitter<UserState> emit) async {
emit(UserLoading());
final user = await userRepository.getUser(event.id);
emit(UserLoaded(user));
}
}
Support request cancellation (optional) #
Future<void> _onSearch(SearchEvent event, Emitter<SearchState> emit) async {
emit(SearchLoading());
final results = await searchRepository.search(
event.query,
token: contextToken, // ← automatic cancellation
);
emit(SearchLoaded(results));
}
🧬 Code generation (optional) #
If you don't want to write if (event is LoadUserEvent) in mapErrorToState,
or override the getErrorMapperForEvent method:
- Add annotation to the class
import 'package:bloc_error_control/annotations.dart';
@BlocErrorControl() // ← add this
class UserBloc extends Bloc<UserEvent, UserState>
with BlocErrorControlMixin<UserEvent, UserState> {
// ...
}
- Add mapper methods with annotation
@ErrorStateFor<LoadUserEvent>(
UserState? onLoadUserError(Object error, StackTrace stack, LoadUserEvent event) {
return UserError('Ошибка загрузки пользователя ${event.id}');
}
@ErrorStateFor<UpdateUserEvent>()
UserState? onUpdateUserError(Object error, StackTrace stack, UpdateUserEvent event) {
return UserError('Ошибка обновления');
}
- Run generation
dart run build_runner build --delete-conflicting-outputs
📊 Performance #
| Metric | Result | Description |
|---|---|---|
| Latency | ~49.4 µs | Delay from add(event) to state emission |
| Throughput | ~69,000 ev/s | Maximum event processing capacity per second |
| Memory Footprint | 0.02 MB | Additional weight of 20,000 active zones/tokens in RAM |
throwIfCancelled |
~0.006 µs | Cost of a single token check inside a loop |
🎯 How it works (briefly) #
- Each event runs in its own Zone
- A unique ICancelToken is created in the zone
- If an error occurs, the mixin:
- Finds the appropriate mapper (local → event-specific → global)
- Transforms the error into a state
- Logs the error
- On repeated events — the previous request is cancelled
- When the bloc is closed — all active requests are cancelled
🧪 Test Coverage #
The package has complete code coverage with tests, including all edge cases and asynchronous scenarios.
| Component | Coverage |
|---|---|
| mixins/ | 100% |
| models/ | 100% |
| annotations/ | 100% |
| exceptions/ | 100% |
| Total | 100% |
Running Tests #
flutter test test/bloc_error_control_mixin/unit/bloc_error_control_test.dart
Branch Coverage
- The core mixin logic has 84.6% branch coverage
Generating Coverage Report:
flutter test --coverage
genhtml coverage/lcov.info -o coverage/html
open coverage/html/index.html
🔧 API Reference #
| Method | Description |
|---|---|
S? mapErrorToState(Object error, StackTrace stack, E event) |
Global error handler |
Опциональные методы #
| Method | Description |
|---|---|
set logger(ErrorLogger logger) |
Plug your own logger |
bool isGlobalSilent(Object error, StackTrace stack) |
Filter "silent" errors |
ICancelToken get contextToken |
Cancellation token for requests |
void cancelTokensByEventType<T>() |
Cancel all requests of a specific type |
List<Map<String, dynamic>> getActiveTokensInfo() |
Diagnostics of active requests |
on() parameters #
| Parameter | Description |
|---|---|
transformer |
Event transformer (sequential(), restartable(), etc.) |
timeout |
Event timeout (default 30 sec) |
⚠️ Requirements #
- Flutter >= 3.0.0
- flutter_bloc >= 8.0.0
- Dart >= 3.0.0
✨ Key features #
- 🛡️ Zone-based error isolation: Catches "wild" asynchronous errors (in timers, microtasks) that would otherwise crash the app.
- 🔌 Universal Token (
contextToken): Automatic cancellation management (CancelToken) for network requests and heavy computations. - 🚀 Near-zero overhead: Processing one event takes less than 1 microsecond.
- 🧹 State cleanliness: Protection against "Zombie Emits" (attempts to change state after the event or Bloc has been closed).
- 🤖 Optional code generation: Automatic creation of error mappers through annotations.
- 🔕 Silent errors: Built-in filtering of cancellations (e.g., Dio cancel) that shouldn't change the UI.
⚙️ Installation #
Add dependencies to your pubspec.yaml:
dependencies:
bloc_error_control: ^1.1.2
dev_dependencies:
bloc_error_control_generator: ^1.1.2
build_runner: ^2.10.0
🔌 Example: Dio Integration for ICancelToken #
For convenient integration with the Dio HTTP client, the package provides the DioCancelTokenX extension, which converts ICancelToken to Dio's CancelToken.
Adding the Extension #
The extension is located in lib/extensions/dio_cancel_token_extension.dart. Copy it to your project or import it from the package:
import 'package:bloc_error_control/extensions/dio_cancel_token_extension.dart';
- Usage
class UserBloc extends Bloc<UserEvent, UserState>
with BlocErrorControlMixin<UserEvent, UserState> {
final Dio _dio = Dio();
Future<void> _onLoadUser(LoadUserEvent event, Emitter<UserState> emit) async {
emit(UserLoading());
final response = await _dio.get(
'https://api.example.com/users/${event.id}',
cancelToken: contextToken.toDio(), // ← conversion
);
emit(UserLoaded(response.data));
}
}
The extension automatically:
- Synchronizes the cancellation state between
ICancelTokenand Dio'sCancelToken - Propagates the cancellation reason for debugging purposes
- Safely handles cases where the token is already cancelled
Complete extension code:
extension DioCancelTokenX on ICancelToken {
/// Converts [ICancelToken] to Dio's [CancelToken].
CancelToken toDio() {
final dioToken = CancelToken();
if (isCancelled) {
_safeCancel(dioToken);
return dioToken;
}
unawaited(
whenCancel.then((_) {
_safeCancel(dioToken);
}).catchError((_) {
// Error ignored
}),
);
return dioToken;
}
void _safeCancel(CancelToken dioToken) {
if (!dioToken.isCancelled) {
final reason = this is EventCancelToken ? (this as EventCancelToken).reason : null;
dioToken.cancel(reason);
}
}
}
📄 License #
MIT © 2025
This software is distributed under the MIT license.