Gestione dello stato con BLoC e Cubit
Cos’è BLoC (Business Logic Component)?
BLoC è un pattern di gestione dello stato progettato per separare la logica di business dalla UI, rendendo il codice più manutenibile, testabile e scalabile. Il nome è l’acronimo di Business Logic Component.
Bloc consente di trasformare eventi (azioni dell’utente o del sistema) in stati, che a loro volta aggiornano la UI.
Bloc si basa sul concetto di Stream: flussi asincroni di eventi e dati. La UI ascolta questi flussi e si ricostruisce automaticamente al cambiare dello stato.
abstract class CounterEvent {}
class Increment extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<Increment>((event, emit) => emit(state + 1));
}
}
Cos’è Cubit?
Cubit
è una versione più semplice e leggera del pattern BLoC. Mentre BLoC utilizza eventi per gestire il flusso delle azioni e trasformarle in stati, un Cubit espone semplicemente metodi pubblici che emettono direttamente nuovi stati.
È ideale per scenari semplici, dove non è necessario avere una gestione complessa di eventi e stati multipli. Inoltre, essendo parte del pacchetto flutter_bloc
, i Cubit possono essere usati insieme a BlocProvider, BlocBuilder e BlocListener.
Perché usare BLoC?
Bloc è particolarmente adatto quando:
- La tua app ha molte schermate interconnesse
- Vuoi mantenere una separazione rigorosa tra logica e presentazione
- Hai bisogno di un flusso dati tracciabile e prevedibile
- Il progetto coinvolge più sviluppatori o team
- La testabilità è un requisito fondamentale
Bloc può sembrare verboso all’inizio, ma permette di costruire app robuste e modulari, con una struttura coerente e facilmente mantenibile nel tempo.
Core Concepts
Streams
I Bloc funzionano tramite Stream, ovvero flussi asincroni di dati. Possiamo immaginarli come tubi nei quali scorrono eventi: la logica riceve un evento in ingresso e produce uno stato in uscita.
Bloc si basa sulla libreria dart:async
e utilizza:
StreamController
per emettere eventiStream
per ascoltare gli stati
Questo approccio garantisce che ogni cambiamento sia reazionato, controllato e sequenziale.
Bloc
Un Bloc è una classe che riceve eventi e restituisce stati. Ogni evento viene gestito da una funzione che elabora la logica (es. API call, trasformazioni) e invoca emit()
per aggiornare lo stato.
class PetBloc extends Bloc<PetEvent, PetState> {
PetBloc() : super(PetInitial()) {
on<LoadPet>(_loadPet);
on<DeletePet>(_deletePet);
}
final List<Pet> _mockDatabase = [
Pet(id: '1', name: 'Fido'),
Pet(id: '2', name: 'Micia'),
];
Future<void> _loadPet(LoadPet event, Emitter<PetState> emit) async {
emit(PetLoading());
await Future.delayed(Duration(milliseconds: 500));
final pet = _mockDatabase.firstWhere((p) => p.id == event.petId);
emit(PetLoaded(pet));
}
Future<void> _deletePet(DeletePet event, Emitter<PetState> emit) async {
emit(PetLoading());
await Future.delayed(Duration(milliseconds: 500));
_mockDatabase.removeWhere((p) => p.id == event.petId);
emit(PetDeleted());
}
}
Bloc Events
Ogni evento rappresenta un’azione. Gli eventi estendono una classe base e utilizzano Equatable
per confronti affidabili.
abstract class PetEvent extends Equatable {
const PetEvent();
@override
List<Object> get props => [];
}
class LoadPet extends PetEvent {
final String petId;
const LoadPet(this.petId);
@override
List<Object> get props => [petId];
}
In Flutter Bloc, Equatable
viene usato per confrontare oggetti in base ai valori anziché al riferimento in memoria. Questo è fondamentale per:
- evitare rebuild inutili
- confrontare eventi o stati con dati uguali
Senza Equatable
, due eventi LoadPet('1')
e LoadPet('1')
non sarebbero uguali. Bloc potrebbe considerarli due eventi diversi anche se hanno lo stesso contenuto.
Quando confronti oggetti in Dart, il confronto di default è per riferimento e non per contenuto. Questo può causare problemi se due oggetti hanno gli stessi valori ma vengono considerati diversi.
Il problema
void main() {
User user = User('Mario');
print(user == User('Mario')); // false
}
class User {
final String name;
User(this.name);
}
Anche se i due oggetti User
hanno lo stesso contenuto, Dart li considera diversi perché sono due istanze diverse in memoria.
La soluzione: Equatable
import 'package:equatable/equatable.dart';
void main() {
User user = User('Mario');
print(user == User('Mario')); // true
}
class User extends Equatable {
final String name;
User(this.name);
@override
List<Object?> get props => [name];
}
Con Equatable
, Dart confronta gli oggetti in base al contenuto della lista props
. Quindi due oggetti con gli stessi valori risultano uguali.
Senza Equatable: più codice da scrivere
class User {
final String name;
User(this.name);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is User && other.name == name);
@override
int get hashCode => name.hashCode;
}
Con Equatable
, tutto questo viene fatto automaticamente sotto il cofano. Meno codice, meno errori.
Uso in Bloc
Bloc usa ==
per decidere se uno stato è cambiato:
- se due eventi/stati sono uguali, Bloc non li elabora o non ricostruisce la UI
Quindi usare Equatable
in Event
e State
è fondamentale per performance e affidabilità.
Dipendenza da aggiungere in pubspec.yaml
dependencies:
equatable: ^2.0.2
Bloc States
Gli stati rappresentano le fasi dell’applicazione. Anche gli stati estendono Equatable
per evitare emissioni duplicate.
abstract class PetState extends Equatable {
const PetState();
@override
List<Object> get props => [];
}
class PetInitial extends PetState {}
class PetLoading extends PetState {}
class PetLoaded extends PetState {
final Pet pet;
const PetLoaded(this.pet);
@override
List<Object> get props => [pet];
}
class PetDeleted extends PetState {}
Se un nuovo stato PetLoaded
viene emesso con lo stesso Pet
, Bloc può riconoscere che non è realmente cambiato e quindi evita un rebuild inutile.
🧩 props: a cosa serve?
La proprietà props
indica quali campi contano nel confronto tra istanze. Bloc la usa per confrontare eventi e stati.
BlocProvider e MultiBlocProvider: Dependency Injection
BlocProvider è un widget che crea e fornisce un bloc ai widget figli tramite il contesto.
BlocProvider
fornisce un bloc o un cubit ai widget figli.MultiBlocProvider
consente di dichiarare più BlocProvider in modo compatto.
BlocProvider(
create: (context) => PetBloc(Provider.of<PetRepo>(context, listen: false)),
child: MyApp(),
)
MultiBlocProvider(
providers: [
BlocProvider(create: (_) => CounterCubit()),
BlocProvider(create: (_) => AuthCubit()),
],
child: MyApp(),
)
Permette di:
- Iniettare dipendenze dall’esterno
- Testare facilmente ogni bloc
- Centralizzare la logica della tua applicazione
BlocBuilder (valido anche per Cubit)
BlocBuilder
è un widget che ricostruisce la UI in base ai cambiamenti di stato del bloc o cubit. È simile a uno StreamBuilder
, ma più semplice da usare.
Expanded(
child: BlocBuilder<PetBloc, PetState>(
builder: (context, state) {
if (state is PetInitial) {
return Center(child: Text('Nessun animale caricato'));
} else if (state is PetLoading) {
return Center(child: CircularProgressIndicator());
} else if (state is PetLoaded) {
return Center(child: Text('Caricato: ${state.pet.name}'));
} else if (state is PetDeleted) {
return Center(child: Text('Animale eliminato'));
} else {
return Center(child: Text('Stato sconosciuto'));
}
},
),
)
BlocBuilder<CounterCubit, int>(
builder: (context, count) {
return Text('$count');
},
)
È possibile specificare una funzione buildWhen
per evitare ricostruzioni non necessarie.
BlocBuilder<CounterCubit, int>(
buildWhen: (previous, current) => current % 2 == 0,
builder: (context, count) => Text('$count'),
)
BlocSelector
BlocSelector
permette di selezionare solo una parte dello stato e ricostruire la UI solo quando quella parte cambia.
BlocSelector<CounterCubit, int, bool>(
selector: (count) => count >= 0,
builder: (context, isPositive) => Text(isPositive ? 'Positivo' : 'Negativo'),
)
BlocListener: Rispondere ai cambiamenti
BlocListener serve per eseguire effetti collaterali (es. mostrare notifiche, cambiare schermata) quando lo stato cambia, senza alterare la UI direttamente.
BlocListener<PetBloc, PetState>(
listener: (context, state) {
if (state is PetDeleted) {
Navigator.of(context).pop();
}
},
child: ChildWidget(),
)
BlocConsumer: Builder + Listener
BlocConsumer unisce BlocBuilder e BlocListener in un unico widget.
BlocConsumer<PetBloc, PetState>(
listener: (context, state) {
if (state is PetUpdated) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Aggiornato!')),
);
}
},
builder: (context, state) {
if (state is PetLoading) return CircularProgressIndicator();
return Container();
},
)
Esempio pratico con Cubit
Creiamo una semplice app contatore con Cubit
, che include due azioni: increment
e decrement
.
counter_cubit.dart
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
void decrement() => emit(state - 1);
}
main.dart
void main() => runApp(CounterApp());
class CounterApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: BlocProvider(
create: (_) => CounterCubit(),
child: CounterPage(),
),
);
}
}
counter_page.dart
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Counter')),
body: BlocBuilder<CounterCubit, int>(
builder: (context, count) => Center(child: Text('$count')),
),
floatingActionButton: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () => context.read<CounterCubit>().increment(),
),
const SizedBox(height: 4),
FloatingActionButton(
child: const Icon(Icons.remove),
onPressed: () => context.read<CounterCubit>().decrement(),
),
],
),
);
}
}
Come puoi vedere, il widget CounterPage
non conosce i dettagli della logica: si limita a notificare CounterCubit
delle azioni dell’utente. La separazione tra UI e logica è netta e pulita.
Bloc vs Cubit
Cubit è una versione più semplice di Bloc: invece di ricevere eventi, espone metodi pubblici per modificare lo stato direttamente.
Aspetto | Cubit | Bloc |
---|---|---|
Complessità | Bassa | Alta |
API | Metodi | Eventi + gestori |
Tracciabilità | Limitata | Eccellente |
Quando usarlo | Logica semplice | Flussi complessi, test, tracking |
Quando usare Cubit?
- Quando la logica è semplice e diretta
- Quando vuoi scrivere meno codice e iniziare velocemente
- Quando sei all’inizio del progetto e vuoi mantenere flessibilità
- Se vuoi passare a Bloc più tardi senza cambiare completamente struttura
Quando usare Bloc?
Bloc è ideale per:
- App complesse e scalabili
- Team di sviluppo con più membri
- Situazioni in cui è importante tracciare ogni interazione
- Codebase altamente modulare e testata
Evita Bloc se:
- L’app è molto piccola o ha pochi stati
- Preferisci codice più conciso (valuta
Cubit
oProvider
)
La combinazione di Bloc
e Cubit
ti dà il meglio di entrambi i mondi: potenza e semplicità. Usa Cubit
per iniziare in modo snello e passa a Bloc
quando la complessità lo richiede.
Esercizi pratici
1. Contatore con Bloc
- Definisci eventi/stati
- Crea un Bloc per il contatore
- Usa BlocProvider e BlocBuilder per collegare logica e UI
2. Bloc Login Form
- Crea due eventi: emailChanged, passwordChanged
- Aggiorna lo stato del form in tempo reale
- Gestisci stati: iniziale, caricamento, errore, successo
3. Todo App
- Bloc che gestisce una lista di task
- La UI mostra solo gli elementi e permette di aggiungere rimuovere o segnare come completata una task
- Aggiorna la lista con eventi Bloc
Buone pratiche
- Definisci eventi e stati separati con
Equatable
- Organizza il codice in cartelle:
bloc
,models
,views
- Usa BlocListener per effetti collaterali
- Usa BlocConsumer per casi combinati
Esercizio Carrello della Spesa con Bloc
Dopo aver implementato il carrello della spesa avanzato con Provider
, ora prova a ricreare la stessa funzionalità usando Bloc. Questo ti aiuterà a consolidare la comprensione della gestione dello stato in modo scalabile.
Obiettivo
Creare un’app con le seguenti funzionalità, riutilizzando la UI già sviluppata:
- Aggiungere un prodotto al carrello
- Incrementare/decrementare la quantità
- Rimuovere un prodotto
- Visualizzare una
Snackbar
con possibilità diUndo
- Mostrare il totale in tempo reale
- Badge dinamico nella bottom bar
- Navigazione tra ProductScreen e CartScreen
Requisiti
- Utilizza
flutter_bloc
eequatable
- Crea:
CartEvent
(es.AddProduct
,RemoveProduct
,Increment
,Decrement
,Undo
)CartState
(che contiene lista prodotti + totale + cronologia per undo)CartBloc
con logica di gestione degli eventi- Modello
Product
conid
,name
,price
,quantity
Suggerimenti
- La lista
_cart
e_history
va spostata dentroCartBloc
- Ogni operazione
add
,remove
, ecc. emette un nuovo stato - Usa
BlocProvider
eBlocBuilder
/BlocListener
per connettere UI e stato - Il badge del carrello può usare
Selector
oppureBlocSelector