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 equatable o freezed per 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.read per le azioni (nei callback) e context.watch/BlocBuilder per 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.