- Published on
Flutter Bloc Examples
- Authors
- Name
- Zairyl Zafra
- @zrylzfra
The Story of Maya's Coffee Shop App
Maya was building her first real Flutter app—a coffee shop ordering system. She started simple, using setState() everywhere. At first, it worked beautifully.
class OrderScreen extends StatefulWidget {
_OrderScreenState createState() => _OrderScreenState();
}
class _OrderScreenState extends State<OrderScreen> {
List<CoffeeItem> cartItems = [];
bool isLoading = false;
String? error;
double total = 0.0;
void addToCart(CoffeeItem item) {
setState(() {
cartItems.add(item);
total += item.price;
});
}
Future<void> placeOrder() async {
setState(() {
isLoading = true;
error = null;
});
try {
await orderService.submitOrder(cartItems);
setState(() {
isLoading = false;
cartItems.clear();
total = 0.0;
});
showSuccessDialog();
} catch (e) {
setState(() {
isLoading = false;
error = e.toString();
});
}
}
Widget build(BuildContext context) {
return Scaffold(
body: isLoading
? CircularProgressIndicator()
: error != null
? ErrorWidget(error: error)
: OrderList(items: cartItems),
);
}
}
But then the requirements grew:
- "Show order history"
- "Add user authentication"
- "Support offline mode"
- "Track order status in real-time"
- "Handle payment processing"
Maya's single screen became 800 lines of tangled setState calls. Testing was impossible. Bugs multiplied. She couldn't sleep.
The Discovery: Enter Bloc
A senior developer introduced Maya to Bloc pattern. "Think of your app like a coffee shop," he explained.
- Events are customer orders ("Add to cart", "Place order", "Cancel")
- Bloc is the barista who processes orders
- States are the results (Loading, Success, Error)
- UI just displays what the barista gives them
"The UI doesn't make coffee. It just shows what's ready."
Understanding Bloc: The Core Concepts
The Three Pillars
- Events - What happened (user actions, API responses)
- States - How things are now (loading, loaded, error)
- Bloc - The logic that transforms events into states
// Event: What the user wants to do
abstract class OrderEvent {}
class AddItemToCart extends OrderEvent {
final CoffeeItem item;
AddItemToCart(this.item);
}
class PlaceOrder extends OrderEvent {}
class RemoveItemFromCart extends OrderEvent {
final String itemId;
RemoveItemFromCart(this.itemId);
}
// State: What's happening now
abstract class OrderState {}
class OrderInitial extends OrderState {}
class OrderLoading extends OrderState {}
class OrderSuccess extends OrderState {
final List<CoffeeItem> items;
final double total;
OrderSuccess({required this.items, required this.total});
}
class OrderError extends OrderState {
final String message;
OrderError(this.message);
}
class OrderPlaced extends OrderState {
final String orderId;
OrderPlaced(this.orderId);
}
// Bloc: The brain that processes events and emits states
class OrderBloc extends Bloc<OrderEvent, OrderState> {
final OrderRepository repository;
List<CoffeeItem> _cartItems = [];
OrderBloc({required this.repository}) : super(OrderInitial()) {
on<AddItemToCart>(_onAddItem);
on<RemoveItemFromCart>(_onRemoveItem);
on<PlaceOrder>(_onPlaceOrder);
}
void _onAddItem(AddItemToCart event, Emitter<OrderState> emit) {
_cartItems.add(event.item);
final total = _cartItems.fold(0.0, (sum, item) => sum + item.price);
emit(OrderSuccess(items: List.from(_cartItems), total: total));
}
void _onRemoveItem(RemoveItemFromCart event, Emitter<OrderState> emit) {
_cartItems.removeWhere((item) => item.id == event.itemId);
final total = _cartItems.fold(0.0, (sum, item) => sum + item.price);
emit(OrderSuccess(items: List.from(_cartItems), total: total));
}
Future<void> _onPlaceOrder(PlaceOrder event, Emitter<OrderState> emit) async {
emit(OrderLoading());
try {
final orderId = await repository.submitOrder(_cartItems);
_cartItems.clear();
emit(OrderPlaced(orderId));
} catch (e) {
emit(OrderError(e.toString()));
}
}
}
Now Maya's UI became incredibly simple:
class OrderScreen extends StatelessWidget {
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => OrderBloc(repository: OrderRepository()),
child: Scaffold(
appBar: AppBar(title: Text('Coffee Shop')),
body: BlocBuilder<OrderBloc, OrderState>(
builder: (context, state) {
if (state is OrderLoading) {
return Center(child: CircularProgressIndicator());
}
if (state is OrderError) {
return ErrorWidget(message: state.message);
}
if (state is OrderPlaced) {
return SuccessScreen(orderId: state.orderId);
}
if (state is OrderSuccess) {
return Column(
children: [
Expanded(
child: ListView.builder(
itemCount: state.items.length,
itemBuilder: (context, index) {
final item = state.items[index];
return CartItemTile(item: item);
},
),
),
TotalBar(total: state.total),
CheckoutButton(),
],
);
}
return EmptyCartWidget();
},
),
),
);
}
}
class CheckoutButton extends StatelessWidget {
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
context.read<OrderBloc>().add(PlaceOrder());
},
child: Text('Place Order'),
);
}
}
Single State vs Multi State: A Deep Comparison
Maya learned that Bloc can handle state in two ways.
Single State Approach
The Concept: One state class holds everything.
// Single state class
class OrderState {
final List<CoffeeItem> items;
final bool isLoading;
final String? error;
final double total;
final String? placedOrderId;
final OrderStatus status; // idle, loading, success, error
OrderState({
this.items = const [],
this.isLoading = false,
this.error,
this.total = 0.0,
this.placedOrderId,
this.status = OrderStatus.idle,
});
// CopyWith for immutability
OrderState copyWith({
List<CoffeeItem>? items,
bool? isLoading,
String? error,
double? total,
String? placedOrderId,
OrderStatus? status,
}) {
return OrderState(
items: items ?? this.items,
isLoading: isLoading ?? this.isLoading,
error: error ?? this.error,
total: total ?? this.total,
placedOrderId: placedOrderId ?? this.placedOrderId,
status: status ?? this.status,
);
}
}
class OrderBloc extends Bloc<OrderEvent, OrderState> {
OrderBloc() : super(OrderState()) {
on<AddItemToCart>(_onAddItem);
on<PlaceOrder>(_onPlaceOrder);
}
void _onAddItem(AddItemToCart event, Emitter<OrderState> emit) {
final newItems = [...state.items, event.item];
final newTotal = newItems.fold(0.0, (sum, item) => sum + item.price);
emit(state.copyWith(
items: newItems,
total: newTotal,
status: OrderStatus.idle,
));
}
Future<void> _onPlaceOrder(PlaceOrder event, Emitter<OrderState> emit) async {
emit(state.copyWith(
isLoading: true,
error: null,
status: OrderStatus.loading,
));
try {
final orderId = await repository.submitOrder(state.items);
emit(state.copyWith(
isLoading: false,
items: [],
total: 0.0,
placedOrderId: orderId,
status: OrderStatus.success,
));
} catch (e) {
emit(state.copyWith(
isLoading: false,
error: e.toString(),
status: OrderStatus.error,
));
}
}
}
// UI with single state
BlocBuilder<OrderBloc, OrderState>(
builder: (context, state) {
if (state.isLoading) {
return CircularProgressIndicator();
}
if (state.error != null) {
return ErrorWidget(message: state.error!);
}
if (state.status == OrderStatus.success) {
return SuccessScreen(orderId: state.placedOrderId);
}
return CartList(items: state.items, total: state.total);
},
)
Pros:
- Simpler to understand initially
- All data in one place
- Easy to serialize for persistence
- Good for simple flows
Cons:
copyWith()becomes huge with many fields- Hard to distinguish between states
- Easy to have invalid combinations (loading=true but error also present)
- Difficult to add new states later
Multi State Approach
The Concept: Different state classes for different situations.
// Multiple state classes
abstract class OrderState {}
class OrderInitial extends OrderState {}
class OrderLoading extends OrderState {
final List<CoffeeItem> currentItems; // Preserve for UI
OrderLoading(this.currentItems);
}
class OrderLoaded extends OrderState {
final List<CoffeeItem> items;
final double total;
OrderLoaded({required this.items, required this.total});
}
class OrderPlacing extends OrderState {
final List<CoffeeItem> items;
OrderPlacing(this.items);
}
class OrderPlaced extends OrderState {
final String orderId;
final DateTime timestamp;
OrderPlaced({required this.orderId, required this.timestamp});
}
class OrderError extends OrderState {
final String message;
final List<CoffeeItem> items; // Preserve cart on error
OrderError({required this.message, required this.items});
}
// Bloc with multi-state
class OrderBloc extends Bloc<OrderEvent, OrderState> {
List<CoffeeItem> _items = [];
OrderBloc() : super(OrderInitial()) {
on<AddItemToCart>(_onAddItem);
on<PlaceOrder>(_onPlaceOrder);
}
void _onAddItem(AddItemToCart event, Emitter<OrderState> emit) {
_items.add(event.item);
final total = _items.fold(0.0, (sum, item) => sum + item.price);
emit(OrderLoaded(
items: List.from(_items),
total: total,
));
}
Future<void> _onPlaceOrder(PlaceOrder event, Emitter<OrderState> emit) async {
emit(OrderPlacing(_items));
try {
final orderId = await repository.submitOrder(_items);
_items.clear();
emit(OrderPlaced(
orderId: orderId,
timestamp: DateTime.now(),
));
} catch (e) {
emit(OrderError(
message: e.toString(),
items: _items, // Keep cart intact
));
}
}
}
// UI with multi-state - more explicit
BlocBuilder<OrderBloc, OrderState>(
builder: (context, state) {
// Type-safe pattern matching
if (state is OrderLoading) {
return LoadingOverlay(items: state.currentItems);
}
if (state is OrderError) {
return Column(
children: [
ErrorBanner(message: state.message),
CartList(items: state.items), // Cart preserved!
],
);
}
if (state is OrderPlaced) {
return SuccessScreen(
orderId: state.orderId,
timestamp: state.timestamp,
);
}
if (state is OrderLoaded) {
return CartView(
items: state.items,
total: state.total,
);
}
return EmptyCartScreen();
},
)
Pros:
- Crystal clear what state you're in
- Impossible to have invalid state combinations
- Each state carries only relevant data
- Easy to add new states without breaking existing ones
- Type-safe pattern matching
- Better IDE autocomplete
Cons:
- More classes to create
- Slightly more boilerplate initially
- Need to think about state transitions
The Comparison Table
| Aspect | Single State | Multi State |
|---|---|---|
| Clarity | All in one place | Explicit state types |
| Type Safety | Manual checks on flags | Compile-time guarantees |
| Invalid States | Possible (loading + error) | Impossible |
| Boilerplate | Less initially | More classes |
| Scalability | Gets messy | Stays clean |
| Testing | Test all combinations | Test each state |
| Refactoring | Risky (break copyWith) | Safe (add new states) |
| Best For | Simple apps, forms | Complex flows, APIs |
Real-World Scenario: Authentication System
Maya needed to build a complete authentication system. Here's where multi-state Bloc shined.
The Requirements
- Login with email/password
- Social media login (Google, Apple)
- Two-factor authentication
- Session management
- Biometric authentication
- Account recovery
The Implementation
// Events
abstract class AuthEvent {}
class LoginRequested extends AuthEvent {
final String email;
final String password;
LoginRequested({required this.email, required this.password});
}
class SocialLoginRequested extends AuthEvent {
final SocialProvider provider;
SocialLoginRequested(this.provider);
}
class TwoFactorCodeSubmitted extends AuthEvent {
final String code;
TwoFactorCodeSubmitted(this.code);
}
class BiometricLoginRequested extends AuthEvent {}
class LogoutRequested extends AuthEvent {}
class SessionChecked extends AuthEvent {}
// States - Multi-state approach
abstract class AuthState {}
class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {}
class Unauthenticated extends AuthState {
final String? message;
Unauthenticated({this.message});
}
class TwoFactorRequired extends AuthState {
final String email;
final String sessionToken;
TwoFactorRequired({
required this.email,
required this.sessionToken,
});
}
class Authenticated extends AuthState {
final User user;
final String token;
final DateTime expiresAt;
Authenticated({
required this.user,
required this.token,
required this.expiresAt,
});
}
class AuthError extends AuthState {
final String message;
final AuthErrorType type;
AuthError({required this.message, required this.type});
}
class SessionExpired extends AuthState {
final String message;
SessionExpired(this.message);
}
// Bloc
class AuthBloc extends Bloc<AuthEvent, AuthState> {
final AuthRepository repository;
final SecureStorage storage;
Timer? _sessionTimer;
AuthBloc({
required this.repository,
required this.storage,
}) : super(AuthInitial()) {
on<LoginRequested>(_onLoginRequested);
on<SocialLoginRequested>(_onSocialLogin);
on<TwoFactorCodeSubmitted>(_onTwoFactorSubmitted);
on<BiometricLoginRequested>(_onBiometricLogin);
on<LogoutRequested>(_onLogout);
on<SessionChecked>(_onSessionChecked);
// Check session on startup
add(SessionChecked());
}
Future<void> _onLoginRequested(
LoginRequested event,
Emitter<AuthState> emit,
) async {
emit(AuthLoading());
try {
final response = await repository.login(
email: event.email,
password: event.password,
);
if (response.requiresTwoFactor) {
emit(TwoFactorRequired(
email: event.email,
sessionToken: response.sessionToken!,
));
} else {
await _handleSuccessfulAuth(response, emit);
}
} on InvalidCredentialsException catch (e) {
emit(AuthError(
message: 'Invalid email or password',
type: AuthErrorType.invalidCredentials,
));
} on NetworkException catch (e) {
emit(AuthError(
message: 'Network error. Please try again.',
type: AuthErrorType.network,
));
} catch (e) {
emit(AuthError(
message: 'An unexpected error occurred',
type: AuthErrorType.unknown,
));
}
}
Future<void> _onSocialLogin(
SocialLoginRequested event,
Emitter<AuthState> emit,
) async {
emit(AuthLoading());
try {
final response = await repository.socialLogin(event.provider);
await _handleSuccessfulAuth(response, emit);
} catch (e) {
emit(AuthError(
message: 'Social login failed',
type: AuthErrorType.socialLogin,
));
}
}
Future<void> _onTwoFactorSubmitted(
TwoFactorCodeSubmitted event,
Emitter<AuthState> emit,
) async {
if (state is! TwoFactorRequired) return;
final currentState = state as TwoFactorRequired;
emit(AuthLoading());
try {
final response = await repository.verifyTwoFactor(
sessionToken: currentState.sessionToken,
code: event.code,
);
await _handleSuccessfulAuth(response, emit);
} catch (e) {
emit(TwoFactorRequired(
email: currentState.email,
sessionToken: currentState.sessionToken,
));
// Could emit error overlay here
}
}
Future<void> _onBiometricLogin(
BiometricLoginRequested event,
Emitter<AuthState> emit,
) async {
emit(AuthLoading());
try {
final authenticated = await repository.authenticateWithBiometrics();
if (authenticated) {
final storedToken = await storage.getToken();
if (storedToken != null) {
final user = await repository.getUserProfile(storedToken);
emit(Authenticated(
user: user,
token: storedToken,
expiresAt: DateTime.now().add(Duration(hours: 24)),
));
_startSessionTimer();
}
} else {
emit(Unauthenticated(message: 'Biometric authentication failed'));
}
} catch (e) {
emit(AuthError(
message: 'Biometric login failed',
type: AuthErrorType.biometric,
));
}
}
Future<void> _onLogout(
LogoutRequested event,
Emitter<AuthState> emit,
) async {
_sessionTimer?.cancel();
await storage.clearToken();
await repository.logout();
emit(Unauthenticated());
}
Future<void> _onSessionChecked(
SessionChecked event,
Emitter<AuthState> emit,
) async {
final token = await storage.getToken();
if (token == null) {
emit(Unauthenticated());
return;
}
try {
final user = await repository.getUserProfile(token);
emit(Authenticated(
user: user,
token: token,
expiresAt: DateTime.now().add(Duration(hours: 24)),
));
_startSessionTimer();
} catch (e) {
emit(SessionExpired('Your session has expired'));
}
}
Future<void> _handleSuccessfulAuth(
AuthResponse response,
Emitter<AuthState> emit,
) async {
await storage.saveToken(response.token);
emit(Authenticated(
user: response.user,
token: response.token,
expiresAt: response.expiresAt,
));
_startSessionTimer();
}
void _startSessionTimer() {
_sessionTimer?.cancel();
_sessionTimer = Timer(Duration(hours: 24), () {
add(SessionChecked());
});
}
Future<void> close() {
_sessionTimer?.cancel();
return super.close();
}
}
// UI Implementation
class AuthPage extends StatelessWidget {
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => AuthBloc(
repository: context.read<AuthRepository>(),
storage: context.read<SecureStorage>(),
),
child: BlocConsumer<AuthBloc, AuthState>(
listener: (context, state) {
if (state is Authenticated) {
Navigator.pushReplacementNamed(context, '/home');
}
if (state is SessionExpired) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.message)),
);
}
},
builder: (context, state) {
if (state is AuthLoading) {
return LoadingScreen();
}
if (state is TwoFactorRequired) {
return TwoFactorScreen(
email: state.email,
onCodeSubmit: (code) {
context.read<AuthBloc>().add(
TwoFactorCodeSubmitted(code),
);
},
);
}
if (state is AuthError) {
return LoginScreen(
errorMessage: state.message,
errorType: state.type,
);
}
return LoginScreen();
},
),
);
}
}
class LoginScreen extends StatefulWidget {
final String? errorMessage;
final AuthErrorType? errorType;
const LoginScreen({this.errorMessage, this.errorType});
_LoginScreenState createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (widget.errorMessage != null)
ErrorBanner(
message: widget.errorMessage!,
type: widget.errorType!,
),
TextField(
controller: _emailController,
decoration: InputDecoration(labelText: 'Email'),
),
SizedBox(height: 16),
TextField(
controller: _passwordController,
decoration: InputDecoration(labelText: 'Password'),
obscureText: true,
),
SizedBox(height: 24),
ElevatedButton(
onPressed: () {
context.read<AuthBloc>().add(LoginRequested(
email: _emailController.text,
password: _passwordController.text,
));
},
child: Text('Login'),
),
SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(Icons.g_mobiledata),
onPressed: () {
context.read<AuthBloc>().add(
SocialLoginRequested(SocialProvider.google),
);
},
),
IconButton(
icon: Icon(Icons.apple),
onPressed: () {
context.read<AuthBloc>().add(
SocialLoginRequested(SocialProvider.apple),
);
},
),
],
),
SizedBox(height: 16),
TextButton(
onPressed: () {
context.read<AuthBloc>().add(BiometricLoginRequested());
},
child: Text('Use Biometric Login'),
),
],
),
),
);
}
}
Why Bloc is Helpful: The Key Benefits
1. Separation of Concerns
Before Bloc:
class MyWidget extends StatefulWidget {
// UI code
// Business logic
// API calls
// State management
// All mixed together!
}
With Bloc:
// UI only cares about display
class MyWidget extends StatelessWidget {
Widget build(context) => BlocBuilder<MyBloc, MyState>(...);
}
// Business logic isolated
class MyBloc extends Bloc<MyEvent, MyState> {
// Pure business logic here
}
// Data layer separate
class MyRepository {
// API calls here
}
2. Testability
void main() {
group('OrderBloc', () {
late OrderBloc bloc;
late MockOrderRepository repository;
setUp(() {
repository = MockOrderRepository();
bloc = OrderBloc(repository: repository);
});
test('adds item to cart', () {
final item = CoffeeItem(id: '1', name: 'Latte', price: 4.50);
bloc.add(AddItemToCart(item));
expectLater(
bloc.stream,
emits(
isA<OrderLoaded>()
.having((s) => s.items.length, 'items length', 1)
.having((s) => s.total, 'total', 4.50),
),
);
});
test('handles order placement error', () async {
when(() => repository.submitOrder(any()))
.thenThrow(Exception('Network error'));
bloc.add(PlaceOrder());
await expectLater(
bloc.stream,
emitsInOrder([
isA<OrderPlacing>(),
isA<OrderError>()
.having((s) => s.message, 'message', contains('Network')),
]),
);
});
});
}
3. Predictable State Flow
Event → Bloc → State → UI
↑ ↓
└──────────────────────┘
Every state change is traceable. No mystery bugs from random setState calls.
4. Time Travel Debugging
BlocObserver can track every event and state:
Event: AddItemToCart(Latte)
State: OrderLoaded(items: [Latte], total: 4.50)
Event: AddItemToCart(Cappuccino)
State: OrderLoaded(items: [Latte, Cappuccino], total: 9.00)
Event: PlaceOrder()
State: OrderPlacing([Latte, Cappuccino])
State: OrderPlaced(orderId: "12345")
5. Scalability
Maya's app grew to 50+ screens. With Bloc:
- Each screen has its own Bloc
- Blocs can communicate via repositories
- Shared state in separate Blocs
- No tangled dependencies
Developer Reminders: Why State Management Matters
🚨 Red Flags That You Need State Management
- setState Hell
setState(() {
isLoading = true;
error = null;
data = null;
showDialog = false;
// ... 20 more variables
});
- Callback Chains
WidgetA(
onChanged: (data) => WidgetB(
onUpdate: (newData) => WidgetC(
onComplete: (result) => WidgetD(...)
)
)
)
- Global State Everywhere
// Globals everywhere - nightmare!
User? currentUser;
List<Item> cart = [];
bool isDarkMode = false;
- Rebuilding Everything
setState(() {
counter++; // Entire tree rebuilds!
});
✅ Benefits of State Management (Bloc)
1. Single Source of Truth
// One place for state
class AuthBloc extends Bloc<AuthEvent, AuthState> {
// All auth state lives here
}
// Use it anywhere
final authState = context.read<AuthBloc>().state;
2. Predictable Updates
// You know exactly when state changes
bloc.add(LoginRequested()); // Explicit!
// Not this:
setState(() { /* mysterious changes */ });
3. Easy to Debug
class MyBlocObserver extends BlocObserver {
void onEvent(Bloc bloc, Object? event) {
print('Event: $event'); // See everything!
}
void onChange(BlocBase bloc, Change change) {
print('${change.currentState} → ${change.nextState}');
}
}
4. Reusable Logic
// Same Bloc, different screens
class ProductListScreen extends StatelessWidget {
Widget build(context) => BlocProvider(
create: (_) => ProductBloc()..add(LoadProducts()),
child: ProductList(),
);
}
class FavoritesScreen extends StatelessWidget {
Widget build(context) => BlocProvider(
create: (_) => ProductBloc()..add(LoadFavorites()),
child: ProductList(),
);
}
5. Performance Optimization
// Only rebuild what changed
BlocBuilder<CartBloc, CartState>(
buildWhen: (previous, current) =>
previous.itemCount != current.itemCount,
builder: (context, state) {
return Text('${state.itemCount} items');
},
)
// vs setState rebuilding entire tree
6. Offline Support Made Easy
class ProductBloc extends Bloc<ProductEvent, ProductState> {
Future<void> _onLoadProducts(
LoadProducts event,
Emitter<ProductState> emit,
) async {
emit(ProductLoading());
try {
// Try network first
final products = await repository.fetchProducts();
await cache.saveProducts(products);
emit(ProductLoaded(products));
} catch (e) {
// Fall back to cache
final cachedProducts = await cache.getProducts();
if (cachedProducts.isNotEmpty) {
emit(ProductLoaded(cachedProducts, fromCache: true));
} else {
emit(ProductError('No connection and no cached data'));
}
}
}
}
📋 When to Use Bloc vs Other Solutions
| Scenario | Use Bloc | Use Provider/setState |
|---|---|---|
| Simple form | ❌ Overkill | ✅ Perfect |
| API integration | ✅ Excellent | ❌ Gets messy |
| Complex flows | ✅ Essential | ❌ Unmaintainable |
| Team project | ✅ Consistent | ⚠️ Varies by dev |
| Testing required | ✅ Built for it | ❌ Hard to test |
| Real-time updates | ✅ Stream-based | ❌ Difficult |
| State persistence | ✅ Easy with hydrated_bloc | ⚠️ Manual work |
🎯 Best Practices for Bloc
1. Name Events Like Actions
// Good - what the user DID
class LoginButtonPressed extends AuthEvent {}
class LogoutRequested extends AuthEvent {}
// Bad - vague or passive
class Login extends AuthEvent {}
class Update extends AuthEvent {}
2. Name States Like Conditions
// Good - describes current condition
class AuthAuthenticated extends AuthState {}
class AuthLoading extends AuthState {}
// Bad - describes actions
class AuthLogin extends AuthState {}
class AuthUpdate extends AuthState {}
3. Keep Blocs Focused
// Good - single responsibility
class ProductBloc // Handles products
class CartBloc // Handles cart
class OrderBloc // Handles orders
// Bad - god bloc
class AppBloc // Handles everything!
4. Use BlocListener for Side Effects
BlocListener<OrderBloc, OrderState>(
listener: (context, state) {
if (state is OrderPlaced) {
// Navigate to success screen
Navigator.pushNamed(context, '/order-success');
// Show notification
showDialog(...);
// Analytics
analytics.logOrderPlaced(state.orderId);
}
},
child: OrderScreen(),
)
5. Combine BlocBuilder and BlocListener
BlocConsumer<AuthBloc, AuthState>(
listener: (context, state) {
// Side effects
if (state is AuthError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.message)),
);
}
},
builder: (context, state) {
// UI based on state
if (state is AuthLoading) {
return LoadingScreen();
}
return LoginScreen();
},
)
Maya's Transformation
After adopting Bloc, Maya's code quality improved dramatically:
Before:
- 800-line widget files
- Nested setState calls
- Impossible to test
- Bugs everywhere
- Scared to add features
After:
- Clean, focused widgets
- Separated business logic
- 100% test coverage
- Predictable behavior
- Confident refactoring
Her coffee shop app now had:
- Real-time order tracking
- Offline support
- Multiple payment methods
- Loyalty program
- Push notifications
All thanks to Bloc's clear architecture.
Final Wisdom
Maya learned that state management isn't about using the fanciest pattern—it's about:
- Keeping UI and logic separate
- Making state changes predictable
- Making your code testable
- Making your app scalable
- Making your life easier
Bloc isn't always needed. A simple counter? Use setState. But for real apps with real complexity? Bloc is your best friend.
"Good architecture is not about prediction, it's about options. Bloc gives you options." - Maya's reflection after building her first production app with confidence