Flutter BLoC Pattern Implementation
Implement BLoC for predictable state management with clear event-state separation.
Overview
The BLoC (Business Logic Component) pattern provides a structured approach to state management that separates business logic from UI completely. Using streams for state emission and events for user actions creates a unidirectional data flow that makes applications predictable and testable. BLoC integrates deeply with Flutter's widget system through BlocWidget and BlocBuilder. BlocBuilder rebuilds widgets in response to state changes, while BlocListener handles side effects like navigation and snackbars. BlocConsumer combines both capabilities, reducing boilerplate when you need both rebuilding and side effect handling. Event classes represent user intentions or system triggers. They should be small, focused, and serializable for debugging. Using freezed or built_value for immutable event and state classes provides excellent equality semantics and reduces bugs from mutable state. The mapEventToState method transforms events into states through async generators, enabling complex async operations with proper loading and error states. For operations that should be cancelable, the transformEvents method can filter or debounce rapid events. This is particularly valuable for search inputs or auto-save functionality.
Code Example
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:equatable/equatable.dart';
import '../models/product.dart';
import '../repositories/product_repository.dart';
part 'product_event.dart';
part 'product_state.dart';
class ProductBloc extends Bloc<ProductEvent, ProductState> {
final ProductRepository repository;
ProductBloc({required this.repository}) : super(ProductInitial()) {
on<LoadProducts>(_onLoadProducts);
on<LoadProductDetail>(_onLoadProductDetail);
on<SearchProducts>(_onSearchProducts);
on<ClearSearch>(_onClearSearch);
}
Future<void> _onLoadProducts(
LoadProducts event,
Emitter<ProductState> emit,
) async {
emit(ProductLoading());
try {
final products = await repository.getProducts(
page: event.page,
category: event.category,
);
if (state is ProductLoaded && event.page > 1) {
final currentProducts = (state as ProductLoaded).products;
emit(ProductLoaded(
products: [...currentProducts, ...products],
hasReachedMax: products.isEmpty,
));
} else {
emit(ProductLoaded(
products: products,
hasReachedMax: products.isEmpty,
));
}
} catch (e) {
emit(ProductError(message: e.toString()));
}
}
Future<void> _onSearchProducts(
SearchProducts event,
Emitter<ProductState> emit,
) async {
if (event.query.isEmpty) {
add(LoadProducts());
return;
}
emit(ProductLoading());
try {
final results = await repository.searchProducts(event.query);
emit(ProductSearchResults(
query: event.query,
results: results,
));
} catch (e) {
emit(ProductError(message: 'Search failed: ' + e.toString()));
}
}
}
// Event definitions
abstract class ProductEvent extends Equatable {
const ProductEvent();
@override
List<Object?> get props => [];
}
class LoadProducts extends ProductEvent {
final int page;
final String? category;
const LoadProducts({this.page = 1, this.category});
@override
List<Object?> get props => [page, category];
}
class LoadProductDetail extends ProductEvent {
final String productId;
const LoadProductDetail(this.productId);
@override
List<Object?> get props => [productId];
}
class SearchProducts extends ProductEvent {
final String query;
const SearchProducts(this.query);
@override
List<Object?> get props => [query];
}
// State definitions
abstract class ProductState extends Equatable {
const ProductState();
@override
List<Object?> get props => [];
}
class ProductInitial extends ProductState {}
class ProductLoading extends ProductState {}
class ProductLoaded extends ProductState {
final List<Product> products;
final bool hasReachedMax;
const ProductLoaded({
required this.products,
this.hasReachedMax = false,
});
ProductLoaded copyWith({
List<Product>? products,
bool? hasReachedMax,
}) {
return ProductLoaded(
products: products ?? this.products,
hasReachedMax: hasReachedMax ?? this.hasReachedMax,
);
}
@override
List<Object?> get props => [products, hasReachedMax];
}
class ProductSearchResults extends ProductState {
final String query;
final List<Product> results;
const ProductSearchResults({
required this.query,
required this.results,
});
@override
List<Object?> get props => [query, results];
}
class ProductError extends ProductState {
final String message;
const ProductError({required this.message});
@override
List<Object?> get props => [message];
}More Flutter Rules
Flutter State Management with Riverpod
Use Riverpod for reactive state management with compile-time safety and testable provider architecture.
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../models/todo.dart';
im...Flutter Clean Architecture Layers
Structure Flutter applications with domain, data, and presentation layers for maintainability.
// Domain Layer - Entities
class Article {
final String id;
final String title;
final String content;
final String authorId;
final DateTime ...Flutter Navigation with GoRouter
Implement declarative routing with GoRouter for deep linking and nested navigation.
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '....