Advanced BLoC Patterns
Battle-tested patterns from the production trenches
Open interactive version (quiz + challenge)Real-world analogy
What is it?
Advanced BLoC patterns are production-grade techniques that handle real-world complexities. The safeEmit pattern from team_mvp_kit prevents crashes when async operations complete after a BLoC is closed. The loading/success/failure tri-state ensures every async operation has proper UI feedback. Typed Failure classes replace raw error strings. BLoC-to-BLoC communication uses stream subscriptions. Event debouncing prevents excessive processing. BlocObserver enables global debugging.
Real-world relevance
In team_mvp_kit, every BLoC uses safeEmit to prevent emit-after-close crashes. Every async operation follows the loading/success/failure pattern so users always see either a spinner, the data, or a helpful error message. Typed failures let the UI show 'No internet connection, tap to retry' versus 'Server error, please try later.' BlocObserver logs every state transition in development builds, making debugging straightforward.
Key points
- The safeEmit Pattern from team_mvp_kit — In team_mvp_kit, safeEmit checks if the BLoC is still active before emitting a state. This prevents the common 'emit after close' error when async operations complete after the user navigates away.
- Loading / Success / Failure Pattern — Every async operation in a production app should handle three states: loading (in progress), success (data received), and failure (something went wrong).
- Failure Types from team_mvp_kit — Instead of raw strings for errors, team_mvp_kit defines typed Failure classes. This lets the UI display specific error messages and take appropriate recovery actions.
- Error Handling in Event Handlers — Every async event handler should wrap its logic in try-catch and emit a failure state. The team_mvp_kit pattern converts exceptions to typed Failures.
- BLoC-to-BLoC Communication — Sometimes one BLoC needs to react to another BLoC's state changes. The recommended approach is to pass one BLoC's stream to another and listen in the constructor.
- BlocObserver for Global Logging — BlocObserver lets you intercept all BLoC events, transitions, and errors globally. It is invaluable for debugging and logging in development.
- Debouncing Events — For search-as-you-type, you do not want to fire an API call for every keystroke. The EventTransformer with debounce prevents excessive processing.
- BlocSelector for Derived State — BlocSelector extracts and transforms just the piece of state a widget needs, only rebuilding when that specific piece changes.
- Testing BLoC with blocTest — The bloc_test package provides blocTest for concise BLoC unit testing. You specify the BLoC, seed state, act (add events), and expect (expected state sequence).
Code example
import 'dart:async';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// ---- Failure types (team_mvp_kit pattern) ----
abstract class Failure extends Equatable {
final String message;
const Failure(this.message);
@override
List<Object?> get props => [message];
}
class ServerFailure extends Failure {
final int? statusCode;
const ServerFailure(super.message, {this.statusCode});
@override
List<Object?> get props => [message, statusCode];
}
class NetworkFailure extends Failure {
const NetworkFailure() : super('No internet connection');
}
// ---- SafeEmit mixin (team_mvp_kit pattern) ----
mixin SafeEmitMixin<E, S> on Bloc<E, S> {
void safeEmit(S newState) {
if (!isClosed) {
// ignore: invalid_use_of_visible_for_testing_member
emit(newState);
}
}
}
// ---- Base classes ----
abstract class BaseEvent extends Equatable {
const BaseEvent();
@override
List<Object?> get props => [];
}
abstract class BaseState extends Equatable {
const BaseState();
@override
List<Object?> get props => [];
}
// ---- Feature: Product catalog ----
class Product {
final String id;
final String name;
final double price;
const Product(this.id, this.name, this.price);
}
// Events
class LoadProducts extends BaseEvent {}
class RefreshProducts extends BaseEvent {}
// State with loading/success/failure
class ProductState extends BaseState {
final List<Product> products;
final bool isLoading;
final Failure? failure;
const ProductState({
this.products = const [],
this.isLoading = false,
this.failure,
});
bool get isSuccess => !isLoading && failure == null;
ProductState copyWith({
List<Product>? products,
bool? isLoading,
Failure? failure,
}) {
return ProductState(
products: products ?? this.products,
isLoading: isLoading ?? this.isLoading,
failure: failure,
);
}
@override
List<Object?> get props => [products, isLoading, failure];
}
// Repository interface
abstract class ProductRepository {
Future<List<Product>> getProducts();
}
// BLoC with safeEmit and error handling
class ProductBloc extends Bloc<BaseEvent, ProductState>
with SafeEmitMixin {
final ProductRepository _repository;
ProductBloc({required ProductRepository repository})
: _repository = repository,
super(const ProductState()) {
on<LoadProducts>(_onLoad);
on<RefreshProducts>(_onRefresh);
}
Future<void> _onLoad(
LoadProducts event,
Emitter<ProductState> emit,
) async {
safeEmit(state.copyWith(isLoading: true, failure: null));
try {
final products = await _repository.getProducts();
safeEmit(state.copyWith(
products: products,
isLoading: false,
));
} catch (e) {
safeEmit(state.copyWith(
isLoading: false,
failure: ServerFailure(e.toString()),
));
}
}
Future<void> _onRefresh(
RefreshProducts event,
Emitter<ProductState> emit,
) async {
try {
final products = await _repository.getProducts();
safeEmit(state.copyWith(products: products));
} catch (e) {
safeEmit(state.copyWith(
failure: ServerFailure(e.toString()),
));
}
}
}
// ---- BlocObserver ----
class AppBlocObserver extends BlocObserver {
@override
void onTransition(Bloc bloc, Transition transition) {
super.onTransition(bloc, transition);
print('${bloc.runtimeType}: '
'${transition.currentState.runtimeType} -> '
'${transition.nextState.runtimeType}');
}
@override
void onError(BlocBase bloc, Object error, StackTrace st) {
super.onError(bloc, error, st);
print('ERROR in ${bloc.runtimeType}: $error');
}
}Line-by-line walkthrough
- 1. Define an abstract Failure class extending Equatable with a message field for all error types.
- 2. ServerFailure adds a statusCode field. NetworkFailure uses a fixed message for connectivity errors.
- 3. SafeEmitMixin checks isClosed before calling emit, preventing the common post-close crash.
- 4. BaseEvent and BaseState extend Equatable with empty props as the foundation for all events and states.
- 5. Product is a simple data class representing catalog items.
- 6. LoadProducts triggers initial fetch. RefreshProducts triggers pull-to-refresh without a loading indicator.
- 7. ProductState follows the loading/success/failure tri-state pattern with typed Failure.
- 8. The isSuccess computed getter returns true when not loading and no failure exists.
- 9. copyWith allows creating new states with selective field overrides.
- 10. ProductRepository is an abstract interface for testability.
- 11. ProductBloc mixes in SafeEmitMixin and takes the repository through constructor injection.
- 12. _onLoad emits loading state with failure cleared, then tries to fetch products.
- 13. On success, safeEmit pushes the loaded products with loading false.
- 14. On failure, the catch block wraps the exception in a ServerFailure.
- 15. _onRefresh skips the loading indicator since pull-to-refresh has its own visual feedback.
- 16. AppBlocObserver logs every state transition and error for debugging across all BLoCs.
Spot the bug
class ProfileBloc extends Bloc<ProfileEvent, ProfileState>
with SafeEmitMixin {
final ProfileRepository _repo;
ProfileBloc(this._repo) : super(const ProfileState()) {
on<LoadProfile>(_onLoad);
}
Future<void> _onLoad(
LoadProfile event,
Emitter<ProfileState> emit,
) async {
safeEmit(state.copyWith(isLoading: true));
final profile = await _repo.getProfile();
safeEmit(state.copyWith(
profile: profile,
isLoading: false,
));
}
}Need a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- BLoC Testing (bloclibrary.dev)
- bloc_concurrency Package (pub.dev)
- BLoC Best Practices (bloclibrary.dev)