Back to skills
SKILL.md
Bloc Advanced
ASecurity| Atributo | Valor | |----------|-------| | **ID** | `flutter-bloc-advanced` | | **Nivel** | π΄ Avanzado | | **VersiΓ³n** | 1.0.0 | | **Keywords** | `bloc`, `cubit`, `state-management-bloc`, `flutter-bloc`, `hydrated-bloc` | | **Referencia** | [BLoC Official Docs](https://bloclibrary.dev/) |
- 4 stars
- 0 votes
- 0 copies
- 2 views
- Added September 8, 2026
Works with
Security analysis
100/100npx -y skills add chimeranext/flutter-boilerplate-monorepo-template --skill bloc-advanced --agent claude-codeAre you the author of Bloc Advanced?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/chimeranext-bloc-advanced)# π¨ Skill: State Management con BLoC Avanzado
## π Metadata
| Atributo | Valor |
|----------|-------|
| **ID** | `flutter-bloc-advanced` |
| **Nivel** | π΄ Avanzado |
| **VersiΓ³n** | 1.0.0 |
| **Keywords** | `bloc`, `cubit`, `state-management-bloc`, `flutter-bloc`, `hydrated-bloc` |
| **Referencia** | [BLoC Official Docs](https://bloclibrary.dev/) |
## π Keywords para InvocaciΓ³n
Usa cualquiera de estos keywords en tus prompts para invocar este skill:
- `bloc`
- `cubit`
- `flutter-bloc`
- `bloc-advanced`
- `hydrated-bloc`
- `@skill:bloc-advanced`
### Ejemplos de Prompts
```
Crea una app con bloc avanzado y persistencia
```
```
Implementa state management con cubit para un mΓ³dulo de productos
```
```
@skill:bloc-advanced - Genera una app con BLoC y manejo de eventos complejos
```
## π DescripciΓ³n
BLoC (Business Logic Component) es un patrΓ³n de gestiΓ³n de estado que separa la lΓ³gica de negocio de la UI mediante streams. Este skill cubre tΓ©cnicas avanzadas como Hydrated BLoC para persistencia, Replay BLoC para debugging, transformers para control de eventos, y estrategias de testing exhaustivas.
**β οΈ IMPORTANTE:** Todos los comandos de este skill deben ejecutarse desde la **raΓz del proyecto** (donde existe el directorio `mobile/`). El skill incluye verificaciones para asegurar que se estΓ‘ en el directorio correcto antes de ejecutar cualquier comando.
### β
CuΓ‘ndo Usar Este Skill
- Aplicaciones enterprise con lΓ³gica compleja
- Necesitas separaciΓ³n estricta entre UI y lΓ³gica
- Requieres testing exhaustivo de lΓ³gica de negocio
- Necesitas persistencia automΓ‘tica del estado
- Quieres replay/undo de estados para debugging
- Aplicaciones con flujos de eventos complejos
- Necesitas transformers para debounce/throttle/retry
### β CuΓ‘ndo NO Usar Este Skill
- Proyectos muy simples (usa setState o Provider)
- El equipo no estΓ‘ familiarizado con reactive programming
- No necesitas la robustez que ofrece BLoC
## ποΈ Estructura del Proyecto
```
lib/
βββ core/
β βββ bloc/
β β βββ bloc_observer.dart
β β βββ app_bloc_observer.dart
β βββ error/
β β βββ failures.dart
β β βββ exceptions.dart
β βββ utils/
β βββ bloc_transformers.dart
β
βββ features/
β βββ auth/
β β βββ data/
β β β βββ datasources/
β β β β βββ auth_local_datasource.dart
β β β β βββ auth_remote_datasource.dart
β β β βββ models/
β β β β βββ user_model.dart
β β β βββ repositories/
β β β βββ auth_repository_impl.dart
β β βββ domain/
β β β βββ entities/
β β β β βββ user.dart
β β β βββ repositories/
β β β β βββ auth_repository.dart
β β β βββ usecases/
β β β βββ login_usecase.dart
β β β βββ logout_usecase.dart
β β β βββ get_current_user_usecase.dart
β β βββ presentation/
β β βββ bloc/
β β β βββ auth_bloc.dart
β β β βββ auth_event.dart
β β β βββ auth_state.dart
β β β βββ login/
β β β βββ login_cubit.dart
β β β βββ login_state.dart
β β βββ screens/
β β β βββ login_screen.dart
β β β βββ register_screen.dart
β β βββ widgets/
β β βββ login_form.dart
β β
β βββ products/
β βββ data/
β β βββ datasources/
β β β βββ product_remote_datasource.dart
β β βββ models/
β β β βββ product_model.dart
β β βββ repositories/
β β βββ product_repository_impl.dart
β βββ domain/
β β βββ entities/
β β β βββ product.dart
β β βββ repositories/
β β β βββ product_repository.dart
β β βββ usecases/
β β βββ get_products_usecase.dart
β β βββ search_products_usecase.dart
β β βββ add_to_cart_usecase.dart
β βββ presentation/
β βββ bloc/
β β βββ products_bloc.dart
β β βββ products_event.dart
β β βββ products_state.dart
β β βββ product_detail/
β β β βββ product_detail_cubit.dart
β β β βββ product_detail_state.dart
β β βββ cart/
β β βββ cart_bloc.dart
β β βββ cart_event.dart
β β βββ cart_state.dart
β βββ screens/
β β βββ products_screen.dart
β β βββ product_detail_screen.dart
β β βββ cart_screen.dart
β βββ widgets/
β βββ product_card.dart
β βββ cart_item.dart
β
βββ main.dart
```
## π¦ Dependencias Requeridas
```yaml
dependencies:
flutter:
sdk: flutter
# BLoC core
flutter_bloc: ^8.1.3
bloc: ^8.1.2
# BLoC extras
hydrated_bloc: ^9.1.2 # Persistencia automΓ‘tica
replay_bloc: ^0.2.3 # Replay/undo functionality
# Utilities
equatable: ^2.0.5 # Para comparaciΓ³n de estados
freezed_annotation: ^2.4.1 # Immutability
json_annotation: ^4.8.1
# Dependency Injection
get_it: ^7.6.4
injectable: ^2.3.2
# Storage para Hydrated BLoC
path_provider: ^2.1.1
dev_dependencies:
# Code generation
build_runner: ^2.4.6
freezed: ^2.4.5
json_serializable: ^6.7.1
injectable_generator: ^2.4.1
# Testing
bloc_test: ^9.1.4
mocktail: ^1.0.1
```
## π» ImplementaciΓ³n
### 1. Setup Inicial con BLoC Observer
#### main.dart
```dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:path_provider/path_provider.dart';
import 'core/bloc/app_bloc_observer.dart';
import 'core/di/injection.dart';
import 'features/auth/presentation/bloc/auth_bloc.dart';
import 'features/auth/presentation/screens/login_screen.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Configurar storage para Hydrated BLoC
HydratedBloc.storage = await HydratedStorage.build(
storageDirectory: await getApplicationDocumentsDirectory(),
);
// Configurar BLoC observer para logging
Bloc.observer = AppBlocObserver();
// Configurar dependency injection
configureDependencies();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
// BLoC global de autenticaciΓ³n
BlocProvider<AuthBloc>(
create: (context) => getIt<AuthBloc>()
..add(const AuthCheckRequested()),
),
// Puedes agregar mΓ‘s BLoCs globales aquΓ
],
child: BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
return MaterialApp(
title: 'BLoC Advanced App',
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
home: state.maybeWhen(
authenticated: (user) => const HomeScreen(),
unauthenticated: () => const LoginScreen(),
orElse: () => const SplashScreen(),
),
);
},
),
);
}
}
```
#### BLoC Observer para Logging
```dart
// lib/core/bloc/app_bloc_observer.dart
import 'package:flutter/foundation.dart';
import 'package:bloc/bloc.dart';
class AppBlocObserver extends BlocObserver {
@override
void onCreate(BlocBase bloc) {
super.onCreate(bloc);
debugPrint('π¦ onCreate -- ${bloc.runtimeType}');
}
@override
void onEvent(Bloc bloc, Object? event) {
super.onEvent(bloc, event);
debugPrint('π¨ onEvent -- ${bloc.runtimeType}, $event');
}
@override
void onChange(BlocBase bloc, Change change) {
super.onChange(bloc, change);
if (kDebugMode) {
debugPrint('π onChange -- ${bloc.runtimeType}');
debugPrint(' currentState: ${change.currentState}');
debugPrint(' nextState: ${change.nextState}');
}
}
@override
void onTransition(Bloc bloc, Transition transition) {
super.onTransition(bloc, transition);
if (kDebugMode) {
debugPrint('π onTransition -- ${bloc.runtimeType}');
debugPrint(' event: ${transition.event}');
debugPrint(' currentState: ${transition.currentState}');
debugPrint(' nextState: ${transition.nextState}');
}
}
@override
void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
debugPrint('β onError -- ${bloc.runtimeType}, $error');
debugPrint('StackTrace: $stackTrace');
super.onError(bloc, error, stackTrace);
}
@override
void onClose(BlocBase bloc) {
super.onClose(bloc);
debugPrint('ποΈ onClose -- ${bloc.runtimeType}');
}
}
```
### 2. BLoC Pattern Completo
#### Domain Layer
```dart
// lib/features/products/domain/entities/product.dart
import 'package:equatable/equatable.dart';
class Product extends Equatable {
final String id;
final String name;
final String description;
final double price;
final String imageUrl;
final int stock;
final List<String> tags;
const Product({
required this.id,
required this.name,
required this.description,
required this.price,
required this.imageUrl,
required this.stock,
this.tags = const [],
});
@override
List<Object?> get props => [id, name, description, price, imageUrl, stock, tags];
}
```
```dart
// lib/features/products/domain/usecases/get_products_usecase.dart
import 'package:dartz/dartz.dart';
import '../../../core/error/failures.dart';
import '../entities/product.dart';
import '../repositories/product_repository.dart';
class GetProductsUseCase {
final ProductRepository repository;
GetProductsUseCase(this.repository);
Future<Either<Failure, List<Product>>> call({
String? category,
String? searchQuery,
int page = 1,
int limit = 20,
}) async {
return await repository.getProducts(
category: category,
searchQuery: searchQuery,
page: page,
limit: limit,
);
}
}
```
#### Presentation Layer - Events
```dart
// lib/features/products/presentation/bloc/products_event.dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'products_event.freezed.dart';
@freezed
class ProductsEvent with _$ProductsEvent {
const factory ProductsEvent.started() = ProductsStarted;
const factory ProductsEvent.loadProducts({
String? category,
@Default(1) int page,
}) = ProductsLoadRequested;
const factory ProductsEvent.refreshProducts() = ProductsRefreshRequested;
const factory ProductsEvent.searchProducts(String query) = ProductsSearchRequested;
const factory ProductsEvent.loadMoreProducts() = ProductsLoadMoreRequested;
const factory ProductsEvent.filterByCategory(String category) = ProductsFilterByCategoryRequested;
const factory ProductsEvent.clearFilters() = ProductsClearFiltersRequested;
}
```
#### Presentation Layer - States
```dart
// lib/features/products/presentation/bloc/products_state.dart
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../domain/entities/product.dart';
part 'products_state.freezed.dart';
@freezed
class ProductsState with _$ProductsState {
const factory ProductsState.initial() = ProductsInitial;
const factory ProductsState.loading() = ProductsLoading;
const factory ProductsState.loaded({
required List<Product> products,
@Default(false) bool hasReachedMax,
@Default(1) int currentPage,
String? category,
String? searchQuery,
}) = ProductsLoaded;
const factory ProductsState.loadingMore({
required List<Product> products,
@Default(1) int currentPage,
String? category,
String? searchQuery,
}) = ProductsLoadingMore;
const factory ProductsState.error(String message) = ProductsError;
}
```
#### Presentation Layer - BLoC
```dart
// lib/features/products/presentation/bloc/products_bloc.dart
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../domain/entities/product.dart';
import '../../domain/usecases/get_products_usecase.dart';
import 'products_event.dart';
import 'products_state.dart';
part 'products_bloc.freezed.dart';
class ProductsBloc extends Bloc<ProductsEvent, ProductsState> {
final GetProductsUseCase getProductsUseCase;
ProductsBloc({
required this.getProductsUseCase,
}) : super(const ProductsState.initial()) {
on<ProductsStarted>(_onStarted);
on<ProductsLoadRequested>(
_onLoadRequested,
transformer: restartable(),
);
on<ProductsRefreshRequested>(_onRefreshRequested);
on<ProductsSearchRequested>(
_onSearchRequested,
transformer: debounce(const Duration(milliseconds: 300)),
);
on<ProductsLoadMoreRequested>(_onLoadMoreRequested);
on<ProductsFilterByCategoryRequested>(_onFilterByCategoryRequested);
on<ProductsClearFiltersRequested>(_onClearFiltersRequested);
}
Future<void> _onStarted(
ProductsStarted event,
Emitter<ProductsState> emit,
) async {
emit(const ProductsState.loading());
await _loadProducts(emit: emit);
}
Future<void> _onLoadRequested(
ProductsLoadRequested event,
Emitter<ProductsState> emit,
) async {
emit(const ProductsState.loading());
await _loadProducts(
emit: emit,
category: event.category,
page: event.page,
);
}
Future<void> _onRefreshRequested(
ProductsRefreshRequested event,
Emitter<ProductsState> emit,
) async {
final currentState = state;
// Mantener filtros si existen
String? category;
String? searchQuery;
currentState.mapOrNull(
loaded: (state) {
category = state.category;
searchQuery = state.searchQuery;
},
);
await _loadProducts(
emit: emit,
category: category,
searchQuery: searchQuery,
page: 1,
);
}
Future<void> _onSearchRequested(
ProductsSearchRequested event,
Emitter<ProductsState> emit,
) async {
emit(const ProductsState.loading());
await _loadProducts(
emit: emit,
searchQuery: event.query,
page: 1,
);
}
Future<void> _onLoadMoreRequested(
ProductsLoadMoreRequested event,
Emitter<ProductsState> emit,
) async {
final currentState = state;
await currentState.mapOrNull(
loaded: (state) async {
if (state.hasReachedMax) return;
final nextPage = state.currentPage + 1;
emit(ProductsState.loadingMore(
products: state.products,
currentPage: state.currentPage,
category: state.category,
searchQuery: state.searchQuery,
));
await _loadProducts(
emit: emit,
category: state.category,
searchQuery: state.searchQuery,
page: nextPage,
existingProducts: state.products,
);
},
);
}
Future<void> _onFilterByCategoryRequested(
ProductsFilterByCategoryRequested event,
Emitter<ProductsState> emit,
) async {
emit(const ProductsState.loading());
await _loadProducts(
emit: emit,
category: event.category,
page: 1,
);
}
Future<void> _onClearFiltersRequested(
ProductsClearFiltersRequested event,
Emitter<ProductsState> emit,
) async {
emit(const ProductsState.loading());
await _loadProducts(emit: emit, page: 1);
}
Future<void> _loadProducts({
required Emitter<ProductsState> emit,
String? category,
String? searchQuery,
int page = 1,
List<Product> existingProducts = const [],
}) async {
final result = await getProductsUseCase(
category: category,
searchQuery: searchQuery,
page: page,
);
result.fold(
(failure) => emit(ProductsState.error(failure.message)),
(newProducts) {
final allProducts = page > 1
? [...existingProducts, ...newProducts]
: newProducts;
emit(ProductsState.loaded(
products: allProducts,
hasReachedMax: newProducts.isEmpty,
currentPage: page,
category: category,
searchQuery: searchQuery,
));
},
);
}
}
// Transformers personalizados
EventTransformer<T> debounce<T>(Duration duration) {
return (events, mapper) => events.debounceTime(duration).flatMap(mapper);
}
EventTransformer<T> restartable<T>() {
return (events, mapper) => events.switchMap(mapper);
}
```
### 3. Cubit Pattern (mΓ‘s simple que BLoC)
```dart
// lib/features/products/presentation/bloc/product_detail/product_detail_cubit.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../../domain/entities/product.dart';
import '../../../domain/usecases/get_product_by_id_usecase.dart';
part 'product_detail_state.dart';
part 'product_detail_cubit.freezed.dart';
class ProductDetailCubit extends Cubit<ProductDetailState> {
final GetProductByIdUseCase getProductByIdUseCase;
ProductDetailCubit({
required this.getProductByIdUseCase,
}) : super(const ProductDetailState.initial());
Future<void> loadProduct(String productId) async {
emit(const ProductDetailState.loading());
final result = await getProductByIdUseCase(productId);
result.fold(
(failure) => emit(ProductDetailState.error(failure.message)),
(product) => emit(ProductDetailState.loaded(product)),
);
}
void incrementQuantity() {
state.mapOrNull(
loaded: (state) {
if (state.quantity < state.product.stock) {
emit(state.copyWith(quantity: state.quantity + 1));
}
},
);
}
void decrementQuantity() {
state.mapOrNull(
loaded: (state) {
if (state.quantity > 1) {
emit(state.copyWith(quantity: state.quantity - 1));
}
},
);
}
void toggleFavorite() {
state.mapOrNull(
loaded: (state) {
emit(state.copyWith(isFavorite: !state.isFavorite));
},
);
}
}
```
```dart
// lib/features/products/presentation/bloc/product_detail/product_detail_state.dart
part of 'product_detail_cubit.dart';
@freezed
class ProductDetailState with _$ProductDetailState {
const factory ProductDetailState.initial() = ProductDetailInitial;
const factory ProductDetailState.loading() = ProductDetailLoading;
const factory ProductDetailState.loaded(
Product product, {
@Default(1) int quantity,
@Default(false) bool isFavorite,
}) = ProductDetailLoaded;
const factory ProductDetailState.error(String message) = ProductDetailError;
}
```
### 4. Hydrated BLoC para Persistencia
```dart
// lib/features/auth/presentation/bloc/auth_bloc.dart
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../domain/entities/user.dart';
import '../../domain/usecases/login_usecase.dart';
import '../../domain/usecases/logout_usecase.dart';
import '../../domain/usecases/get_current_user_usecase.dart';
import 'auth_event.dart';
import 'auth_state.dart';
part 'auth_bloc.freezed.dart';
part 'auth_bloc.g.dart';
class AuthBloc extends HydratedBloc<AuthEvent, AuthState> {
final LoginUseCase loginUseCase;
final LogoutUseCase logoutUseCase;
final GetCurrentUserUseCase getCurrentUserUseCase;
AuthBloc({
required this.loginUseCase,
required this.logoutUseCase,
required this.getCurrentUserUseCase,
}) : super(const AuthState.initial()) {
on<AuthCheckRequested>(_onCheckRequested);
on<AuthLoginRequested>(_onLoginRequested);
on<AuthLogoutRequested>(_onLogoutRequested);
}
Future<void> _onCheckRequested(
AuthCheckRequested event,
Emitter<AuthState> emit,
) async {
emit(const AuthState.loading());
final result = await getCurrentUserUseCase();
result.fold(
(failure) => emit(const AuthState.unauthenticated()),
(user) => emit(AuthState.authenticated(user)),
);
}
Future<void> _onLoginRequested(
AuthLoginRequested event,
Emitter<AuthState> emit,
) async {
emit(const AuthState.loading());
final result = await loginUseCase(
email: event.email,
password: event.password,
);
result.fold(
(failure) => emit(AuthState.error(failure.message)),
(user) => emit(AuthState.authenticated(user)),
);
}
Future<void> _onLogoutRequested(
AuthLogoutRequested event,
Emitter<AuthState> emit,
) async {
await logoutUseCase();
emit(const AuthState.unauthenticated());
}
// Persistencia: serializar estado a JSON
@override
AuthState? fromJson(Map<String, dynamic> json) {
try {
return AuthState.fromJson(json);
} catch (_) {
return null;
}
}
// Persistencia: deserializar estado de JSON
@override
Map<String, dynamic>? toJson(AuthState state) {
// Solo persistir estado authenticated
return state.maybeMap(
authenticated: (state) => state.toJson(),
orElse: () => null,
);
}
}
```
### 5. Replay BLoC para Debugging
```dart
// lib/features/products/presentation/bloc/cart/cart_bloc.dart
import 'package:replay_bloc/replay_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../domain/entities/product.dart';
import 'cart_event.dart';
import 'cart_state.dart';
part 'cart_bloc.freezed.dart';
class CartBloc extends ReplayBloc<CartEvent, CartState> {
CartBloc() : super(const CartState.empty()) {
on<CartAddProduct>(_onAddProduct);
on<CartRemoveProduct>(_onRemoveProduct);
on<CartUpdateQuantity>(_onUpdateQuantity);
on<CartClear>(_onClear);
}
void _onAddProduct(CartAddProduct event, Emitter<CartState> emit) {
state.map(
empty: (_) => emit(CartState.loaded(
items: {event.product.id: CartItem(product: event.product, quantity: 1)},
)),
loaded: (state) {
final items = Map<String, CartItem>.from(state.items);
if (items.containsKey(event.product.id)) {
final existingItem = items[event.product.id]!;
items[event.product.id] = existingItem.copyWith(
quantity: existingItem.quantity + 1,
);
} else {
items[event.product.id] = CartItem(
product: event.product,
quantity: 1,
);
}
emit(state.copyWith(items: items));
},
);
}
void _onRemoveProduct(CartRemoveProduct event, Emitter<CartState> emit) {
state.mapOrNull(
loaded: (state) {
final items = Map<String, CartItem>.from(state.items);
items.remove(event.productId);
if (items.isEmpty) {
emit(const CartState.empty());
} else {
emit(state.copyWith(items: items));
}
},
);
}
void _onUpdateQuantity(CartUpdateQuantity event, Emitter<CartState> emit) {
state.mapOrNull(
loaded: (state) {
final items = Map<String, CartItem>.from(state.items);
final item = items[event.productId];
if (item != null) {
if (event.quantity <= 0) {
items.remove(event.productId);
} else {
items[event.productId] = item.copyWith(quantity: event.quantity);
}
if (items.isEmpty) {
emit(const CartState.empty());
} else {
emit(state.copyWith(items: items));
}
}
},
);
}
void _onClear(CartClear event, Emitter<CartState> emit) {
emit(const CartState.empty());
}
}
// Uso de Replay BLoC en UI
class CartScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => CartBloc(),
child: Scaffold(
appBar: AppBar(
title: Text('Cart'),
actions: [
// Botones de undo/redo
BlocBuilder<CartBloc, CartState>(
builder: (context, state) {
final bloc = context.read<CartBloc>();
return Row(
children: [
IconButton(
icon: Icon(Icons.undo),
onPressed: bloc.canUndo ? bloc.undo : null,
),
IconButton(
icon: Icon(Icons.redo),
onPressed: bloc.canRedo ? bloc.redo : null,
),
],
);
},
),
],
),
body: BlocBuilder<CartBloc, CartState>(
builder: (context, state) {
return state.map(
empty: (_) => Center(child: Text('Cart is empty')),
loaded: (state) => ListView.builder(
itemCount: state.items.length,
itemBuilder: (context, index) {
final item = state.items.values.elementAt(index);
return CartItemWidget(item: item);
},
),
);
},
),
),
);
}
}
```
### 6. Uso de BLoC en Widgets
#### BlocBuilder
```dart
class ProductsScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Products')),
body: BlocBuilder<ProductsBloc, ProductsState>(
builder: (context, state) {
return state.map(
initial: (_) => Center(child: Text('Press button to load')),
loading: (_) => Center(child: CircularProgressIndicator()),
loaded: (state) => ProductsList(products: state.products),
loadingMore: (state) => ProductsList(
products: state.products,
isLoadingMore: true,
),
error: (state) => ErrorWidget(message: state.message),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
context.read<ProductsBloc>().add(
const ProductsEvent.loadProducts(),
);
},
child: Icon(Icons.refresh),
),
);
}
}
```
#### BlocListener para Side Effects
```dart
class LoginScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => LoginCubit(
loginUseCase: getIt<LoginUseCase>(),
),
child: Scaffold(
appBar: AppBar(title: Text('Login')),
body: BlocListener<LoginCubit, LoginState>(
listener: (context, state) {
// Side effects aquΓ
state.mapOrNull(
success: (state) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Login successful!')),
);
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => HomeScreen()),
);
},
error: (state) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
backgroundColor: Colors.red,
),
);
},
);
},
child: LoginForm(),
),
),
);
}
}
```
#### BlocConsumer (Builder + Listener combinados)
```dart
class ProductDetailScreen extends StatelessWidget {
final String productId;
const ProductDetailScreen({required this.productId});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => ProductDetailCubit(
getProductByIdUseCase: getIt<GetProductByIdUseCase>(),
)..loadProduct(productId),
child: Scaffold(
appBar: AppBar(title: Text('Product Detail')),
body: BlocConsumer<ProductDetailCubit, ProductDetailState>(
listener: (context, state) {
// Side effects
state.mapOrNull(
error: (state) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.message)),
);
},
);
},
builder: (context, state) {
// UI
return state.map(
initial: (_) => SizedBox.shrink(),
loading: (_) => Center(child: CircularProgressIndicator()),
loaded: (state) => ProductDetailContent(
product: state.product,
quantity: state.quantity,
isFavorite: state.isFavorite,
),
error: (state) => ErrorWidget(message: state.message),
);
},
),
),
);
}
}
```
### 7. Testing con BLoC Test
```dart
// test/features/products/presentation/bloc/products_bloc_test.dart
import 'package:bloc_test/bloc_test.dart';
import 'package:dartz/dartz.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
class MockGetProductsUseCase extends Mock implements GetProductsUseCase {}
void main() {
late ProductsBloc bloc;
late MockGetProductsUseCase mockGetProductsUseCase;
setUp(() {
mockGetProductsUseCase = MockGetProductsUseCase();
bloc = ProductsBloc(getProductsUseCase: mockGetProductsUseCase);
});
tearDown(() {
bloc.close();
});
group('ProductsBloc', () {
final tProducts = [
Product(
id: '1',
name: 'Product 1',
description: 'Description 1',
price: 10.0,
imageUrl: 'url1',
stock: 5,
),
Product(
id: '2',
name: 'Product 2',
description: 'Description 2',
price: 20.0,
imageUrl: 'url2',
stock: 10,
),
];
test('initial state is ProductsInitial', () {
expect(bloc.state, equals(const ProductsState.initial()));
});
blocTest<ProductsBloc, ProductsState>(
'emits [loading, loaded] when load products succeeds',
build: () {
when(() => mockGetProductsUseCase(
category: any(named: 'category'),
searchQuery: any(named: 'searchQuery'),
page: any(named: 'page'),
limit: any(named: 'limit'),
)).thenAnswer((_) async => Right(tProducts));
return bloc;
},
act: (bloc) => bloc.add(const ProductsEvent.loadProducts()),
expect: () => [
const ProductsState.loading(),
ProductsState.loaded(
products: tProducts,
hasReachedMax: false,
currentPage: 1,
),
],
verify: (_) {
verify(() => mockGetProductsUseCase(
category: null,
searchQuery: null,
page: 1,
limit: 20,
)).called(1);
},
);
blocTest<ProductsBloc, ProductsState>(
'emits [loading, error] when load products fails',
build: () {
when(() => mockGetProductsUseCase(
category: any(named: 'category'),
searchQuery: any(named: 'searchQuery'),
page: any(named: 'page'),
limit: any(named: 'limit'),
)).thenAnswer((_) async => Left(ServerFailure('Server error')));
return bloc;
},
act: (bloc) => bloc.add(const ProductsEvent.loadProducts()),
expect: () => [
const ProductsState.loading(),
const ProductsState.error('Server error'),
],
);
blocTest<ProductsBloc, ProductsState>(
'emits correct states when loading more products',
build: () {
when(() => mockGetProductsUseCase(
category: any(named: 'category'),
searchQuery: any(named: 'searchQuery'),
page: any(named: 'page'),
limit: any(named: 'limit'),
)).thenAnswer((_) async => Right(tProducts));
return bloc;
},
seed: () => ProductsState.loaded(
products: tProducts,
currentPage: 1,
),
act: (bloc) => bloc.add(const ProductsEvent.loadMoreProducts()),
expect: () => [
ProductsState.loadingMore(products: tProducts, currentPage: 1),
ProductsState.loaded(
products: [...tProducts, ...tProducts],
currentPage: 2,
),
],
);
blocTest<ProductsBloc, ProductsState>(
'debounces search events',
build: () {
when(() => mockGetProductsUseCase(
category: any(named: 'category'),
searchQuery: any(named: 'searchQuery'),
page: any(named: 'page'),
limit: any(named: 'limit'),
)).thenAnswer((_) async => Right(tProducts));
return bloc;
},
act: (bloc) async {
bloc.add(const ProductsEvent.searchProducts('test1'));
bloc.add(const ProductsEvent.searchProducts('test2'));
bloc.add(const ProductsEvent.searchProducts('test3'));
},
wait: const Duration(milliseconds: 400),
expect: () => [
const ProductsState.loading(),
ProductsState.loaded(
products: tProducts,
searchQuery: 'test3',
currentPage: 1,
),
],
verify: (_) {
// Solo debe llamar una vez debido al debounce
verify(() => mockGetProductsUseCase(
category: null,
searchQuery: 'test3',
page: 1,
limit: 20,
)).called(1);
},
);
});
}
```
## π― Mejores PrΓ‘cticas
### 1. Event Naming
β
**DO:**
```dart
const factory ProductsEvent.loadProducts() = ProductsLoadRequested;
const factory ProductsEvent.refreshProducts() = ProductsRefreshRequested;
```
β **DON'T:**
```dart
const factory ProductsEvent.load() = LoadProducts; // Poco descriptivo
const factory ProductsEvent.getProducts() = GetProducts; // Usa verbos de UI
```
### 2. State Naming y Estructura
β
**DO:**
```dart
@freezed
class ProductsState with _$ProductsState {
const factory ProductsState.initial() = ProductsInitial;
const factory ProductsState.loading() = ProductsLoading;
const factory ProductsState.loaded({
required List<Product> products,
@Default(false) bool hasReachedMax,
}) = ProductsLoaded;
const factory ProductsState.error(String message) = ProductsError;
}
```
β **DON'T:**
```dart
// No uses un solo estado con flags
class ProductsState {
final List<Product> products;
final bool isLoading;
final bool hasError;
final String? errorMessage;
}
```
### 3. Uso de Transformers
β
**DO:**
```dart
on<ProductsSearchRequested>(
_onSearchRequested,
transformer: debounce(const Duration(milliseconds: 300)),
);
on<ProductsLoadRequested>(
_onLoadRequested,
transformer: restartable(), // Cancela eventos anteriores
);
```
### 4. SeparaciΓ³n de Concerns
β
**DO:**
```dart
// BLoC solo coordina
class ProductsBloc extends Bloc<ProductsEvent, ProductsState> {
final GetProductsUseCase getProductsUseCase; // Use case hace el trabajo
Future<void> _onLoadRequested(...) async {
final result = await getProductsUseCase(); // Delega al use case
// ... maneja resultado
}
}
```
β **DON'T:**
```dart
// BLoC con lΓ³gica de negocio acoplada
class ProductsBloc extends Bloc<ProductsEvent, ProductsState> {
final Dio dio;
Future<void> _onLoadRequested(...) async {
final response = await dio.get('/products'); // β LΓ³gica de API aquΓ
final products = (response.data as List).map(...).toList(); // β Parsing aquΓ
}
}
```
### 5. Testing Exhaustivo
β
**DO:**
```dart
// Usa bloc_test para tests claros y concisos
blocTest<ProductsBloc, ProductsState>(
'emits [loading, loaded] when successful',
build: () => ProductsBloc(getProductsUseCase: mockUseCase),
act: (bloc) => bloc.add(const ProductsEvent.loadProducts()),
expect: () => [
const ProductsState.loading(),
ProductsState.loaded(products: tProducts),
],
);
```
### 6. Manejo de Streams y Subscriptions
β
**DO:**
```dart
class NotificationsBloc extends Bloc<NotificationsEvent, NotificationsState> {
final NotificationService _notificationService;
StreamSubscription<Notification>? _notificationSubscription;
NotificationsBloc(this._notificationService) : super(...) {
on<NotificationsStarted>(_onStarted);
on<NotificationsReceived>(_onReceived);
}
Future<void> _onStarted(...) async {
await _notificationSubscription?.cancel();
_notificationSubscription = _notificationService.stream.listen(
(notification) => add(NotificationsEvent.received(notification)),
);
}
@override
Future<void> close() {
_notificationSubscription?.cancel();
return super.close();
}
}
```
## π Recursos Adicionales
- [BLoC Official Documentation](https://bloclibrary.dev/)
- [Hydrated BLoC](https://pub.dev/packages/hydrated_bloc)
- [Replay BLoC](https://pub.dev/packages/replay_bloc)
- [BLoC Test](https://pub.dev/packages/bloc_test)
- [BLoC Architecture Tutorial](https://bloclibrary.dev/#/architecture)
## π Skills Relacionados
- [Clean Architecture](../clean-architecture/SKILL.md) - Arquitectura completa con BLoC
- [Testing Strategy](../testing/SKILL.md) - Testing de BLoCs
- [Riverpod](../riverpod/SKILL.md) - Alternativa a BLoC
---
**VersiΓ³n:** 1.0.0
**Γltima actualizaciΓ³n:** Diciembre 2025
Attribution
Related skills
View all in development βComments
Loading commentsβ¦
Newsletter
Get the weekly list of safe new skills, and the ones we pulled.