Skip to Content
FlutterLezione 3 Gestione Dello Stato3.5 Gestione dello stato (BLoC)

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 eventi
  • Stream 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.

AspettoCubitBloc
ComplessitàBassaAlta
APIMetodiEventi + gestori
TracciabilitàLimitataEccellente
Quando usarloLogica sempliceFlussi 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 o Provider)

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à di Undo
  • Mostrare il totale in tempo reale
  • Badge dinamico nella bottom bar
  • Navigazione tra ProductScreen e CartScreen

Requisiti

  • Utilizza flutter_bloc e equatable
  • 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 con id, name, price, quantity

Suggerimenti

  • La lista _cart e _history va spostata dentro CartBloc
  • Ogni operazione add, remove, ecc. emette un nuovo stato
  • Usa BlocProvider e BlocBuilder/BlocListener per connettere UI e stato
  • Il badge del carrello può usare Selector oppure BlocSelector