Cos'è il pattern BLoC
BLoC (Business Logic Component) è uno dei pattern di gestione dello stato più diffusi nell'ecosistema Flutter. La sua idea centrale è semplice: separare la logica di business dall'interfaccia utente, facendo comunicare i due livelli tramite flussi di eventi in ingresso e stati in uscita.
La libreria flutter_bloc, mantenuta da Felix Angelov, offre un'implementazione robusta e testabile di questo pattern. In questo articolo vedremo sia i Cubit (la versione semplificata) sia i Bloc veri e propri basati su eventi.
Installazione
Aggiungi le dipendenze al tuo pubspec.yaml:
dependencies:
flutter_bloc: ^8.1.6
equatable: ^2.0.5
equatable non è obbligatorio ma semplifica il confronto tra stati, evitando ricostruzioni inutili della UI.
Iniziare con Cubit
Un Cubit è la forma più semplice di gestione dello stato: espone metodi che emettono nuovi stati tramite emit(). Vediamo un classico contatore.
import 'package:flutter_bloc/flutter_bloc.dart';
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
void decrement() => emit(state - 1);
}
Per usarlo nella UI, forniamo il Cubit tramite BlocProvider e reagiamo ai cambiamenti con BlocBuilder.
BlocProvider(
create: (_) => CounterCubit(),
child: const CounterView(),
);
class CounterView extends StatelessWidget {
const CounterView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: BlocBuilder<CounterCubit, int>(
builder: (context, count) => Text('$count', style: const TextStyle(fontSize: 48)),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read<CounterCubit>().increment(),
child: const Icon(Icons.add),
),
);
}
}
Passare a Bloc: eventi e stati
Quando la logica cresce, il Bloc basato su eventi offre maggiore struttura e tracciabilità. Modelliamo il caricamento di una lista di utenti.
Definire gli eventi
import 'package:equatable/equatable.dart';
abstract class UserEvent extends Equatable {
const UserEvent();
@override
List<Object?> get props => [];
}
class UsersRequested extends UserEvent {
const UsersRequested();
}
Definire gli stati
abstract class UserState extends Equatable {
const UserState();
@override
List<Object?> get props => [];
}
class UserInitial extends UserState {}
class UserLoading extends UserState {}
class UserLoaded extends UserState {
final List<String> users;
const UserLoaded(this.users);
@override
List<Object?> get props => [users];
}
class UserError extends UserState {
final String message;
const UserError(this.message);
@override
List<Object?> get props => [message];
}
Implementare il Bloc
class UserBloc extends Bloc<UserEvent, UserState> {
final UserRepository repository;
UserBloc(this.repository) : super(UserInitial()) {
on<UsersRequested>(_onUsersRequested);
}
Future<void> _onUsersRequested(
UsersRequested event,
Emitter<UserState> emit,
) async {
emit(UserLoading());
try {
final users = await repository.fetchUsers();
emit(UserLoaded(users));
} catch (e) {
emit(UserError('Impossibile caricare gli utenti'));
}
}
}
Reagire agli stati nella UI
Usiamo BlocBuilder per costruire l'interfaccia in base allo stato corrente:
BlocBuilder<UserBloc, UserState>(
builder: (context, state) {
if (state is UserLoading) {
return const Center(child: CircularProgressIndicator());
} else if (state is UserLoaded) {
return ListView(
children: state.users.map((u) => ListTile(title: Text(u))).toList(),
);
} else if (state is UserError) {
return Center(child: Text(state.message));
}
return const SizedBox.shrink();
},
)
BlocListener e BlocConsumer
Non tutto quello che accade in uno stato deve ricostruire la UI. Per azioni "una tantum" — come mostrare uno SnackBar o navigare — si usa BlocListener:
BlocListener<UserBloc, UserState>(
listener: (context, state) {
if (state is UserError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.message)),
);
}
},
child: const MyView(),
)
Quando serve sia costruire la UI sia reagire con effetti collaterali, BlocConsumer combina builder e listener in un unico widget.
Ottimizzare con buildWhen
Per evitare ricostruzioni superflue, BlocBuilder accetta il parametro buildWhen, che decide se ricostruire in base allo stato precedente e a quello nuovo:
BlocBuilder<UserBloc, UserState>(
buildWhen: (previous, current) => current is UserLoaded,
builder: (context, state) {
// ...
},
)
Best practice
- Un Bloc per feature: mantieni ogni Bloc focalizzato su una singola responsabilità.
- Stati immutabili: usa
equatableofreezedper garantire confronti corretti. - Nessuna logica di UI nel Bloc: il Bloc non deve mai importare
flutter/material.dart. - Testa la logica: i Bloc sono facilmente testabili con il pacchetto
bloc_test. - Usa
context.readper le azioni (nei callback) econtext.watch/BlocBuilderper la lettura reattiva.
Conclusione
Il pattern BLoC con flutter_bloc offre una separazione netta tra logica e presentazione, un flusso dei dati prevedibile e un'ottima testabilità. Inizia con i Cubit per casi semplici e passa ai Bloc basati su eventi quando la complessità cresce: avrai un'architettura solida e scalabile.
