Gestione dello stato in Flutter con il pattern BLoC e flutter_bloc
GuideIntermedio35 min Flutter 3.x

Gestione dello stato in Flutter con il pattern BLoC e flutter_bloc

Il pattern BLoC (Business Logic Component) è uno degli approcci più diffusi e scalabili per gestire lo stato nelle applicazioni Flutter. L'idea di fondo è semplice: la UI invia eventi a un BLoC, il BLoC elabora la logica di business ed emette stati a cui la UI reagisce. In questo modo logica e interfaccia restano completamente separate, rendendo il codice più testabile e manutenibile.

In questo tutorial useremo il pacchetto ufficiale flutter_bloc per costruire prima un classico contatore e poi una piccola app che carica dati in modo asincrono gestendo gli stati di caricamento, successo ed errore. Al termine avrai una base solida per applicare BLoC nei tuoi progetti reali.

  1. 1

    Aggiungere le dipendenze

    Per iniziare aggiungiamo i pacchetti flutter_bloc (che integra BLoC con Flutter) e bloc (il core indipendente dal framework). Il pacchetto equatable è opzionale ma molto utile: ci permette di confrontare gli stati senza scrivere manualmente l'override di == e hashCode.

    Apri il file pubspec.yaml e aggiungi le dipendenze, oppure usa il comando da terminale.

    flutter pub add flutter_bloc equatable

    Risultato atteso

    Le dipendenze flutter_bloc, bloc ed equatable risultano installate nel pubspec.yaml.

  2. 2

    Definire gli eventi del Counter

    Nel pattern BLoC la UI non modifica direttamente lo stato, ma invia eventi. Definiamo una classe base astratta CounterEvent e due eventi concreti: incremento e decremento.

    Usiamo Equatable così due eventi dello stesso tipo vengono considerati uguali, utile soprattutto nei test.

    import 'package:equatable/equatable.dart';
    
    abstract class CounterEvent extends Equatable {
      const CounterEvent();
    
      @override
      List<Object?> get props => [];
    }
    
    class CounterIncremented extends CounterEvent {
      const CounterIncremented();
    }
    
    class CounterDecremented extends CounterEvent {
      const CounterDecremented();
    }

    Risultato atteso

    Abbiamo un file counter_event.dart con la gerarchia degli eventi.

  3. 3

    Creare il Bloc che gestisce gli eventi

    Ora creiamo il CounterBloc. Estende Bloc<CounterEvent, int>: il primo tipo è l'evento in ingresso, il secondo è lo stato emesso (qui un semplice int).

    Nel costruttore impostiamo lo stato iniziale a 0 e registriamo gli handler con on<Evento>. Ogni handler riceve l'evento e un oggetto emit con cui pubblicare il nuovo stato.

    import 'package:flutter_bloc/flutter_bloc.dart';
    import 'counter_event.dart';
    
    class CounterBloc extends Bloc<CounterEvent, int> {
      CounterBloc() : super(0) {
        on<CounterIncremented>((event, emit) => emit(state + 1));
        on<CounterDecremented>((event, emit) => emit(state - 1));
      }
    }

    Risultato atteso

    Il CounterBloc reagisce agli eventi emettendo un nuovo valore intero.

  4. 4

    Fornire il Bloc all'albero dei widget

    Per rendere il BLoC disponibile alla UI usiamo BlocProvider. Lo posizioniamo sopra il widget che ne ha bisogno (qui sopra CounterPage).

    BlocProvider si occupa anche di chiudere automaticamente il BLoC quando viene rimosso dall'albero, evitando memory leak.

    import 'package:flutter/material.dart';
    import 'package:flutter_bloc/flutter_bloc.dart';
    import 'counter_bloc.dart';
    import 'counter_page.dart';
    
    void main() => runApp(const MyApp());
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'BLoC Demo',
          home: BlocProvider(
            create: (_) => CounterBloc(),
            child: const CounterPage(),
          ),
        );
      }
    }

    Risultato atteso

    Il CounterBloc è accessibile da tutti i widget figli di CounterPage.

  5. 5

    Costruire la UI con BlocBuilder e inviare eventi

    Usiamo BlocBuilder per ricostruire solo la parte di UI che dipende dallo stato del contatore. Per inviare eventi recuperiamo il BLoC con context.read<CounterBloc>() e chiamiamo add.

    È buona pratica usare context.read per inviare eventi (non serve ascoltare) e BlocBuilder o context.watch solo dove serve ricostruire la UI.

    import 'package:flutter/material.dart';
    import 'package:flutter_bloc/flutter_bloc.dart';
    import 'counter_bloc.dart';
    import 'counter_event.dart';
    
    class CounterPage extends StatelessWidget {
      const CounterPage({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('Contatore BLoC')),
          body: Center(
            child: BlocBuilder<CounterBloc, int>(
              builder: (context, count) => Text(
                '$count',
                style: Theme.of(context).textTheme.displayMedium,
              ),
            ),
          ),
          floatingActionButton: Column(
            mainAxisAlignment: MainAxisAlignment.end,
            children: [
              FloatingActionButton(
                heroTag: 'inc',
                onPressed: () =>
                    context.read<CounterBloc>().add(const CounterIncremented()),
                child: const Icon(Icons.add),
              ),
              const SizedBox(height: 12),
              FloatingActionButton(
                heroTag: 'dec',
                onPressed: () =>
                    context.read<CounterBloc>().add(const CounterDecremented()),
                child: const Icon(Icons.remove),
              ),
            ],
          ),
        );
      }
    }

    Risultato atteso

    Premendo i pulsanti il numero a schermo aumenta e diminuisce in tempo reale.

  6. 6

    Gestire stati asincroni con stati multipli

    Nei casi reali lo stato non è un semplice intero. Vediamo come modellare il caricamento di dati con stati distinti: iniziale, caricamento, successo ed errore.

    Definiamo una gerarchia di stati e un BLoC che, alla ricezione di un evento UsersRequested, emette prima UsersLoadInProgress e poi UsersLoadSuccess o UsersLoadFailure. Nota l'uso di async nell'handler e di emit multipli.

    import 'package:bloc/bloc.dart';
    import 'package:equatable/equatable.dart';
    
    // Eventi
    abstract class UsersEvent extends Equatable {
      @override
      List<Object?> get props => [];
    }
    
    class UsersRequested extends UsersEvent {}
    
    // Stati
    abstract class UsersState extends Equatable {
      @override
      List<Object?> get props => [];
    }
    
    class UsersInitial extends UsersState {}
    class UsersLoadInProgress extends UsersState {}
    
    class UsersLoadSuccess extends UsersState {
      final List<String> users;
      UsersLoadSuccess(this.users);
      @override
      List<Object?> get props => [users];
    }
    
    class UsersLoadFailure extends UsersState {
      final String message;
      UsersLoadFailure(this.message);
      @override
      List<Object?> get props => [message];
    }
    
    // Bloc
    class UsersBloc extends Bloc<UsersEvent, UsersState> {
      UsersBloc() : super(UsersInitial()) {
        on<UsersRequested>((event, emit) async {
          emit(UsersLoadInProgress());
          try {
            await Future.delayed(const Duration(seconds: 1));
            emit(UsersLoadSuccess(['Mario', 'Giulia', 'Luca']));
          } catch (e) {
            emit(UsersLoadFailure('Errore nel caricamento'));
          }
        });
      }
    }

    Risultato atteso

    Il UsersBloc emette in sequenza lo stato di caricamento e poi successo o errore.

  7. 7

    Reagire a tutti gli stati nella UI

    Con BlocBuilder possiamo usare lo switch sui diversi tipi di stato per mostrare uno spinner, la lista o un messaggio di errore. Per effetti collaterali una tantum (snackbar, navigazione) si usa invece BlocListener, mentre BlocConsumer combina entrambi.

    Questo schema rende la UI dichiarativa e perfettamente sincronizzata con lo stato del BLoC.

    BlocBuilder<UsersBloc, UsersState>(
      builder: (context, state) {
        if (state is UsersLoadInProgress) {
          return const Center(child: CircularProgressIndicator());
        }
        if (state is UsersLoadSuccess) {
          return ListView(
            children: state.users
                .map((u) => ListTile(title: Text(u)))
                .toList(),
          );
        }
        if (state is UsersLoadFailure) {
          return Center(child: Text(state.message));
        }
        // UsersInitial
        return Center(
          child: ElevatedButton(
            onPressed: () =>
                context.read<UsersBloc>().add(UsersRequested()),
            child: const Text('Carica utenti'),
          ),
        );
      },
    )

    Risultato atteso

    La UI mostra il pulsante iniziale, poi lo spinner e infine la lista degli utenti o l'errore.

CondividiXLinkedInFacebookWhatsApp

Commenti (0)

Ancora nessun commento. Inizia tu!