Gestione dello Stato in Flutter: Riverpod vs BLoC - Guida Comparativa Completa

Confronto dettagliato tra Riverpod 3.x e BLoC 9.x per la gestione dello stato in Flutter. Architettura, retry automatico, testabilità e casi d'uso per scegliere la soluzione migliore.

Confronto tra Riverpod e BLoC per la gestione dello stato in Flutter

La gestione dello stato rappresenta una sfida centrale nello sviluppo Flutter. Riverpod e BLoC dominano l'ecosistema, ognuno con una filosofia distinta. Questa guida confronta le due soluzioni attraverso implementazioni concrete per orientare la scelta in base alle esigenze del progetto.

Prerequisiti

Questa guida presuppone familiarità con Flutter e con i fondamentali della gestione dello stato. Gli esempi utilizzano Riverpod 3.x e flutter_bloc 9.x, le versioni stabili attuali nel 2026.

Filosofie Centrali dei Due Approcci

Riverpod e BLoC risolvono lo stesso problema con paradigmi opposti. Comprendere queste differenze concettuali permette di scegliere lo strumento giusto per ogni contesto.

Riverpod adotta un approccio dichiarativo e reattivo. I provider definiscono fonti di dati che i widget osservano. Il framework gestisce automaticamente il ciclo di vita, il caching e le dipendenze tra provider. La versione 3.0 ha introdotto miglioramenti significativi: retry automatico per provider falliti, pause/resume dei listener in base alla visibilità del widget e persistenza offline sperimentale.

BLoC (Business Logic Component) impone un'architettura rigorosa orientata agli eventi. I componenti emettono eventi, il Bloc li elabora e produce nuovi stati. Questa separazione esplicita facilita il tracciamento del flusso di dati. La versione 9.x ha introdotto EmittableStateStreamableSource per una maggiore flessibilità nei test.

riverpod_philosophy.dartdart
// Riverpod 3.x: Notifier replaces StateNotifier as the standard pattern

class Counter extends _$Counter {
  
  int build() => 0;
  
  void increment() => state++;
}

// Usage in a widget
class CounterWidget extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    // Reactive read: automatic rebuild if value changes
    final count = ref.watch(counterProvider);
    return Text('$count');
  }
}
bloc_philosophy.dartdart
// BLoC 9.x: explicit events/states separation
sealed class CounterEvent {}
final class IncrementPressed extends CounterEvent {}

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    // Each event has its dedicated handler
    on<IncrementPressed>((event, emit) => emit(state + 1));
  }
}

// Usage in a widget
class CounterWidget extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return BlocBuilder<CounterBloc, int>(
      builder: (context, count) => Text('$count'),
    );
  }
}

La scelta tra questi approcci dipende dalle preferenze del team e dai vincoli del progetto. Per una panoramica delle opzioni di state management incluso GetX, consultare il confronto completo di state management Flutter per il 2026.

Configurazione Iniziale

La configurazione iniziale rivela differenze ergonomiche tra le due soluzioni. Riverpod privilegia la semplicità, BLoC offre più struttura.

Installazione di Riverpod

Riverpod richiede un singolo pacchetto e un wrapper alla radice dell'applicazione. La generazione di codice con riverpod_generator è l'approccio raccomandato nella versione 3.x.

main.dartdart
// Riverpod configuration: single wrapper at root
import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() {
  runApp(
    // ProviderScope wraps the entire application
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: HomeScreen(),
    );
  }
}

Installazione di BLoC

BLoC richiede più pacchetti e una configurazione più elaborata con BlocProvider per ogni Bloc utilizzato.

main.dartdart
// BLoC configuration: explicit providers for each Bloc
import 'package:flutter_bloc/flutter_bloc.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    // MultiBlocProvider for multiple Blocs
    return MultiBlocProvider(
      providers: [
        BlocProvider(create: (_) => AuthBloc()),
        BlocProvider(create: (_) => ThemeBloc()),
      ],
      child: MaterialApp(
        home: HomeScreen(),
      ),
    );
  }
}

La configurazione di BLoC richiede più codice iniziale ma rende esplicite le dipendenze fin dall'inizio.

Gestione dello Stato Semplice: Contatori e Toggle

I casi semplici illustrano l'ergonomia quotidiana di ciascuna soluzione. Riverpod eccelle in concisione, BLoC mantiene la sua struttura orientata agli eventi.

Contatore con Riverpod (Pattern Notifier)

Riverpod 3.x depreca StateProvider e StateNotifierProvider a favore dell'API unificata Notifier. Il nuovo pattern è più pulito e funziona perfettamente con la generazione di codice.

counter_riverpod.dartdart
// Notifier pattern: the standard in Riverpod 3.x

class Counter extends _$Counter {
  
  int build() => 0;

  void increment() => state++;
  void decrement() => state--;
  void reset() => state = 0;
}

class CounterScreen extends ConsumerWidget {
  const CounterScreen({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    // watch for reactive value
    final count = ref.watch(counterProvider);

    return Scaffold(
      body: Center(child: Text('Count: $count')),
      floatingActionButton: FloatingActionButton(
        // read for actions (no rebuild)
        onPressed: () => ref.read(counterProvider.notifier).increment(),
        child: const Icon(Icons.add),
      ),
    );
  }
}

Contatore con BLoC

counter_bloc.dartdart
// Typed events for each possible action
sealed class CounterEvent {}
final class CounterIncremented extends CounterEvent {}
final class CounterDecremented extends CounterEvent {}
final class CounterReset extends CounterEvent {}

// Bloc with handlers for each event
class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<CounterIncremented>((event, emit) => emit(state + 1));
    on<CounterDecremented>((event, emit) => emit(state - 1));
    on<CounterReset>((event, emit) => emit(0));
  }
}

class CounterScreen extends StatelessWidget {
  const CounterScreen({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: BlocBuilder<CounterBloc, int>(
          builder: (context, count) => Text('Count: $count'),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        // Event dispatch to modify state
        onPressed: () => context.read<CounterBloc>().add(CounterIncremented()),
        child: const Icon(Icons.add),
      ),
    );
  }
}

Per i casi semplici, Riverpod riduce significativamente il boilerplate. BLoC diventa rilevante quando la logica acquista complessità o quando la tracciabilità degli eventi è importante.

Provider Legacy in Riverpod 3.x

StateProvider e StateNotifierProvider funzionano ancora ma richiedono l'import da flutter_riverpod/legacy.dart. Per nuovo codice, utilizzare Notifier o AsyncNotifier.

Gestione dello Stato Asincrono: Chiamate API

Le operazioni asincrone rivelano la potenza di ogni soluzione. Gestire gli stati di caricamento, errore e dati costituisce una sfida importante. Riverpod 3.x introduce il retry automatico con backoff esponenziale per i provider falliti.

Dati Asincroni con Riverpod

async_riverpod.dartdart
// AsyncNotifier: automatic loading/error/data management with retry

class Users extends _$Users {
  
  Future<List<User>> build() async {
    // autoDispose releases resources when provider is no longer used
    // Automatic retry on failure (exponential backoff: 200ms to 6.4s)
    final repository = ref.watch(userRepositoryProvider);
    return repository.fetchUsers();
  }

  Future<void> refresh() async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() => build());
  }
}

class UsersScreen extends ConsumerWidget {
  const UsersScreen({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    final usersAsync = ref.watch(usersProvider);

    // when handles all 3 possible states
    return usersAsync.when(
      loading: () => const Center(child: CircularProgressIndicator()),
      error: (error, stack) => Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Error: $error'),
            ElevatedButton(
              // invalidate forces reload
              onPressed: () => ref.invalidate(usersProvider),
              child: const Text('Retry'),
            ),
          ],
        ),
      ),
      data: (users) => ListView.builder(
        itemCount: users.length,
        itemBuilder: (context, index) => UserTile(user: users[index]),
      ),
    );
  }
}

Dati Asincroni con BLoC

async_bloc.dartdart
// Explicit states for each loading phase
sealed class UsersState {}
final class UsersInitial extends UsersState {}
final class UsersLoading extends UsersState {}
final class UsersLoaded extends UsersState {
  final List<User> users;
  UsersLoaded(this.users);
}
final class UsersError extends UsersState {
  final String message;
  UsersError(this.message);
}

// Events to trigger actions
sealed class UsersEvent {}
final class UsersFetchRequested extends UsersEvent {}
final class UsersRefreshRequested extends UsersEvent {}

class UsersBloc extends Bloc<UsersEvent, UsersState> {
  final UserRepository _repository;

  UsersBloc(this._repository) : super(UsersInitial()) {
    on<UsersFetchRequested>(_onFetchRequested);
    on<UsersRefreshRequested>(_onRefreshRequested);
  }

  Future<void> _onFetchRequested(
    UsersFetchRequested event,
    Emitter<UsersState> emit,
  ) async {
    emit(UsersLoading());
    try {
      final users = await _repository.fetchUsers();
      emit(UsersLoaded(users));
    } catch (e) {
      emit(UsersError(e.toString()));
    }
  }

  Future<void> _onRefreshRequested(
    UsersRefreshRequested event,
    Emitter<UsersState> emit,
  ) async {
    // Keep current state during refresh
    final currentState = state;
    try {
      final users = await _repository.fetchUsers();
      emit(UsersLoaded(users));
    } catch (e) {
      // Restore previous state on error
      if (currentState is UsersLoaded) {
        emit(currentState);
      } else {
        emit(UsersError(e.toString()));
      }
    }
  }
}
users_screen_bloc.dartdart
// Widget with pattern matching on states
class UsersScreen extends StatelessWidget {
  const UsersScreen({super.key});

  
  Widget build(BuildContext context) {
    return BlocBuilder<UsersBloc, UsersState>(
      builder: (context, state) {
        return switch (state) {
          UsersInitial() => Center(
              child: ElevatedButton(
                onPressed: () => context.read<UsersBloc>().add(UsersFetchRequested()),
                child: const Text('Load'),
              ),
            ),
          UsersLoading() => const Center(child: CircularProgressIndicator()),
          UsersError(:final message) => Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text('Error: $message'),
                  ElevatedButton(
                    onPressed: () => context
                        .read<UsersBloc>()
                        .add(UsersFetchRequested()),
                    child: const Text('Retry'),
                  ),
                ],
              ),
            ),
          UsersLoaded(:final users) => ListView.builder(
              itemCount: users.length,
              itemBuilder: (context, index) => UserTile(user: users[index]),
            ),
        };
      },
    );
  }
}

BLoC offre un controllo granulare su ogni transizione di stato. Riverpod automatizza di più tramite AsyncValue e logica di retry integrata.

Pronto a superare i tuoi colloqui su Flutter?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

Dipendenze tra Stati: Composizione e Iniezione

Le applicazioni reali coinvolgono stati interdipendenti. La gestione di queste dipendenze differenzia notevolmente i due approcci.

Composizione con Riverpod

composition_riverpod.dartdart
// Base provider: configuration

ApiClient apiClient(Ref ref) {
  final baseUrl = ref.watch(environmentProvider).apiUrl;
  return ApiClient(baseUrl: baseUrl);
}

// Dependent provider: repository

ProductRepository productRepository(Ref ref) {
  // Automatic client injection
  final client = ref.watch(apiClientProvider);
  return ProductRepository(client);
}

// Provider with parameter: product by ID

Future<Product> product(Ref ref, String productId) async {
  final repository = ref.watch(productRepositoryProvider);
  return repository.getProduct(productId);
}

// Derived provider: filtered products

List<Product> filteredProducts(Ref ref) {
  final products = ref.watch(productsProvider).valueOrNull ?? [];
  final filter = ref.watch(productFilterProvider);

  return products.where((p) => p.category == filter.category).toList();
}

// Usage with parameter
class ProductDetailScreen extends ConsumerWidget {
  final String productId;

  const ProductDetailScreen({super.key, required this.productId});

  
  Widget build(BuildContext context, WidgetRef ref) {
    // family allows passing parameters
    final productAsync = ref.watch(productProvider(productId));

    return productAsync.when(
      loading: () => const ProductSkeleton(),
      error: (e, _) => ErrorWidget(error: e),
      data: (product) => ProductDetails(product: product),
    );
  }
}

Composizione con BLoC

composition_bloc.dartdart
// Repository injected into the Bloc
class ProductBloc extends Bloc<ProductEvent, ProductState> {
  final ProductRepository _repository;
  final CartBloc _cartBloc;
  late final StreamSubscription _cartSubscription;

  ProductBloc({
    required ProductRepository repository,
    required CartBloc cartBloc,
  })  : _repository = repository,
        _cartBloc = cartBloc,
        super(ProductInitial()) {
    on<ProductFetchRequested>(_onFetchRequested);
    on<ProductAddedToCart>(_onAddedToCart);

    // Listen to cart changes
    _cartSubscription = _cartBloc.stream.listen((cartState) {
      // React to cart changes
      if (cartState is CartUpdated) {
        add(ProductCartSyncRequested(cartState.items));
      }
    });
  }

  Future<void> _onFetchRequested(
    ProductFetchRequested event,
    Emitter<ProductState> emit,
  ) async {
    emit(ProductLoading());
    try {
      final product = await _repository.getProduct(event.productId);
      // Check if product is in cart
      final isInCart = _cartBloc.state.contains(product.id);
      emit(ProductLoaded(product, isInCart: isInCart));
    } catch (e) {
      emit(ProductError(e.toString()));
    }
  }

  
  Future<void> close() {
    _cartSubscription.cancel();
    return super.close();
  }
}

// Configuration with dependency injection
class ProductsPage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => ProductBloc(
        repository: context.read<ProductRepository>(),
        cartBloc: context.read<CartBloc>(),
      )..add(ProductFetchRequested()),
      child: const ProductsView(),
    );
  }
}

Riverpod gestisce le dipendenze in modo dichiarativo. BLoC richiede gestione manuale delle sottoscrizioni tra Bloc.

Testabilità e Mocking

Il testing costituisce un criterio decisivo per i progetti professionali. Entrambe le soluzioni eccellono in questo ambito con approcci diversi. Le competenze di testing sono frequentemente valutate nei colloqui Flutter.

Test con Riverpod

test_riverpod.dartdart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';

class MockUserRepository extends Mock implements UserRepository {}

void main() {
  group('UserProvider Tests', () {
    late MockUserRepository mockRepository;

    setUp(() {
      mockRepository = MockUserRepository();
    });

    test('returns users from repository', () async {
      // Arrange
      final expectedUsers = [User(id: '1', name: 'Test')];
      when(() => mockRepository.fetchUsers())
          .thenAnswer((_) async => expectedUsers);

      // ProviderContainer.test auto-disposes after test
      final container = ProviderContainer.test(
        overrides: [
          userRepositoryProvider.overrideWithValue(mockRepository),
        ],
      );

      // Act
      final users = await container.read(usersProvider.future);

      // Assert
      expect(users, expectedUsers);
      verify(() => mockRepository.fetchUsers()).called(1);
    });

    test('handles repository errors', () async {
      when(() => mockRepository.fetchUsers())
          .thenThrow(Exception('Network error'));

      final container = ProviderContainer.test(
        overrides: [
          userRepositoryProvider.overrideWithValue(mockRepository),
        ],
      );

      expect(
        () => container.read(usersProvider.future),
        throwsException,
      );
    });
  });
}

Test con BLoC

test_bloc.dartdart
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';

class MockUserRepository extends Mock implements UserRepository {}

void main() {
  group('UsersBloc Tests', () {
    late MockUserRepository mockRepository;

    setUp(() {
      mockRepository = MockUserRepository();
    });

    // blocTest simplifies state sequence testing
    blocTest<UsersBloc, UsersState>(
      'emits [Loading, Loaded] when fetch succeeds',
      build: () {
        when(() => mockRepository.fetchUsers())
            .thenAnswer((_) async => [User(id: '1', name: 'Test')]);
        return UsersBloc(mockRepository);
      },
      act: (bloc) => bloc.add(UsersFetchRequested()),
      expect: () => [
        isA<UsersLoading>(),
        isA<UsersLoaded>().having(
          (s) => s.users.length,
          'users count',
          1,
        ),
      ],
    );

    blocTest<UsersBloc, UsersState>(
      'emits [Loading, Error] when fetch fails',
      build: () {
        when(() => mockRepository.fetchUsers())
            .thenThrow(Exception('Network error'));
        return UsersBloc(mockRepository);
      },
      act: (bloc) => bloc.add(UsersFetchRequested()),
      expect: () => [
        isA<UsersLoading>(),
        isA<UsersError>(),
      ],
    );
  });
}

Il pacchetto bloc_test offre una sintassi dedicata per testare sequenze di stato. Riverpod 3.x aggiunge ProviderContainer.test() per la pulizia automatica e NotifierProvider.overrideWithBuild() per il mock del solo metodo build.

Copertura dei Test

Testare solo i casi nominali è insufficiente. I test devono coprire errori di rete, timeout, stati limite e transizioni di stato inaspettate. Per pratiche di testing complete, consultare la guida al testing Flutter.

Prestazioni e Ottimizzazione dei Rebuild

Le prestazioni impattano direttamente sull'esperienza utente. Entrambe le soluzioni offrono meccanismi di ottimizzazione distinti.

Ottimizzazione con Riverpod

perf_riverpod.dartdart
// select to rebuild only if targeted value changes
class UserNameWidget extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    // Rebuilds only if user.name changes
    final name = ref.watch(userProvider.select((user) => user.name));
    return Text(name);
  }
}

// Provider with automatic caching

ExpensiveResult expensiveComputation(Ref ref) {
  final input = ref.watch(inputProvider);
  // Computation automatically cached
  return performExpensiveComputation(input);
}

// keepAlive with timer for temporary caching

Future<List<Product>> searchResults(Ref ref, String query) async {
  // Temporary keepAlive during typing
  final link = ref.keepAlive();

  // Timer to release after inactivity
  final timer = Timer(const Duration(seconds: 30), link.close);
  ref.onDispose(timer.cancel);

  return searchProducts(query);
}

Ottimizzazione con BLoC

perf_bloc.dartdart
// buildWhen limits rebuilds conditionally
class UserNameWidget extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return BlocBuilder<UserBloc, UserState>(
      // Rebuilds only if name changes
      buildWhen: (previous, current) {
        if (previous is UserLoaded && current is UserLoaded) {
          return previous.user.name != current.user.name;
        }
        return true;
      },
      builder: (context, state) {
        if (state is UserLoaded) {
          return Text(state.user.name);
        }
        return const SizedBox.shrink();
      },
    );
  }
}

// BlocSelector to extract a specific value
class UserAvatarWidget extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return BlocSelector<UserBloc, UserState, String?>(
      // Select only the avatar URL
      selector: (state) => state is UserLoaded ? state.user.avatarUrl : null,
      builder: (context, avatarUrl) {
        if (avatarUrl == null) return const DefaultAvatar();
        return NetworkImage(avatarUrl);
      },
    );
  }
}

Entrambe le soluzioni offrono ottimizzazioni granulari. Riverpod con select, BLoC con buildWhen e BlocSelector. Per tecniche avanzate di ottimizzazione delle prestazioni, consultare la guida all'ottimizzazione delle prestazioni Flutter.

Caso Pratico: Autenticazione Completa

Un sistema di autenticazione illustra i pattern reali per ogni soluzione. Questo caso combina stato persistente, chiamate API e navigazione.

Autenticazione con Riverpod

auth_riverpod.dartdart
// Authentication state with sealed class
sealed class AuthState {
  const AuthState();
}
final class AuthInitial extends AuthState {
  const AuthInitial();
}
final class AuthLoading extends AuthState {
  const AuthLoading();
}
final class AuthAuthenticated extends AuthState {
  final User user;
  const AuthAuthenticated(this.user);
}
final class AuthUnauthenticated extends AuthState {
  final String? error;
  const AuthUnauthenticated([this.error]);
}

// Notifier to manage auth state

class Auth extends _$Auth {
  
  Future<AuthState> build() async {
    final storage = ref.watch(secureStorageProvider);
    final repository = ref.watch(authRepositoryProvider);
    
    final token = await storage.getToken();
    if (token != null) {
      try {
        final user = await repository.getCurrentUser(token);
        return AuthAuthenticated(user);
      } catch (_) {
        await storage.deleteToken();
        return const AuthUnauthenticated();
      }
    }
    return const AuthUnauthenticated();
  }

  Future<void> login(String email, String password) async {
    state = const AsyncLoading();
    final repository = ref.read(authRepositoryProvider);
    final storage = ref.read(secureStorageProvider);
    
    try {
      final result = await repository.login(email, password);
      await storage.saveToken(result.token);
      state = AsyncData(AuthAuthenticated(result.user));
    } catch (e) {
      state = AsyncData(AuthUnauthenticated(e.toString()));
    }
  }

  Future<void> logout() async {
    final storage = ref.read(secureStorageProvider);
    await storage.deleteToken();
    state = const AsyncData(AuthUnauthenticated());
  }
}

// Redirect based on auth state

GoRouter router(Ref ref) {
  final authState = ref.watch(authProvider);

  return GoRouter(
    redirect: (context, state) {
      final isAuth = authState.valueOrNull is AuthAuthenticated;
      final isAuthRoute = state.matchedLocation.startsWith('/auth');

      if (!isAuth && !isAuthRoute) return '/auth/login';
      if (isAuth && isAuthRoute) return '/home';
      return null;
    },
    routes: [...],
  );
}

Autenticazione con BLoC

auth_bloc.dartdart
// Exhaustive states for authentication
sealed class AuthState {
  const AuthState();
}
final class AuthInitial extends AuthState {
  const AuthInitial();
}
final class AuthCheckInProgress extends AuthState {
  const AuthCheckInProgress();
}
final class AuthLoginInProgress extends AuthState {
  const AuthLoginInProgress();
}
final class AuthSuccess extends AuthState {
  final User user;
  const AuthSuccess(this.user);
}
final class AuthFailure extends AuthState {
  final String error;
  const AuthFailure(this.error);
}
final class AuthLoggedOut extends AuthState {
  const AuthLoggedOut();
}

// Authentication events
sealed class AuthEvent {
  const AuthEvent();
}
final class AuthCheckRequested extends AuthEvent {
  const AuthCheckRequested();
}
final class AuthLoginSubmitted extends AuthEvent {
  final String email;
  final String password;
  const AuthLoginSubmitted(this.email, this.password);
}
final class AuthLogoutRequested extends AuthEvent {
  const AuthLogoutRequested();
}

class AuthBloc extends Bloc<AuthEvent, AuthState> {
  final AuthRepository _repository;
  final SecureStorage _storage;

  AuthBloc({
    required AuthRepository repository,
    required SecureStorage storage,
  })  : _repository = repository,
        _storage = storage,
        super(const AuthInitial()) {
    on<AuthCheckRequested>(_onCheckRequested);
    on<AuthLoginSubmitted>(_onLoginSubmitted);
    on<AuthLogoutRequested>(_onLogoutRequested);
  }

  Future<void> _onCheckRequested(
    AuthCheckRequested event,
    Emitter<AuthState> emit,
  ) async {
    emit(const AuthCheckInProgress());
    final token = await _storage.getToken();

    if (token == null) {
      emit(const AuthLoggedOut());
      return;
    }

    try {
      final user = await _repository.getCurrentUser(token);
      emit(AuthSuccess(user));
    } catch (_) {
      await _storage.deleteToken();
      emit(const AuthLoggedOut());
    }
  }

  Future<void> _onLoginSubmitted(
    AuthLoginSubmitted event,
    Emitter<AuthState> emit,
  ) async {
    emit(const AuthLoginInProgress());
    try {
      final result = await _repository.login(event.email, event.password);
      await _storage.saveToken(result.token);
      emit(AuthSuccess(result.user));
    } catch (e) {
      emit(AuthFailure(e.toString()));
    }
  }

  Future<void> _onLogoutRequested(
    AuthLogoutRequested event,
    Emitter<AuthState> emit,
  ) async {
    await _storage.deleteToken();
    emit(const AuthLoggedOut());
  }
}

Entrambe le implementazioni gestiscono la stessa funzionalità con stili diversi. BLoC esplicita ogni transizione, Riverpod semplifica la sintassi.

Tabella Comparativa Riassuntiva

CriterioRiverpod 3.xBLoC 9.x
Curva di apprendimentoModerataPiù ripida
BoilerplateMinimo (con codegen)Significativo
Type SafetyEccellenteEccellente
TestabilitàEccellenteEccellente
Retry automaticoIntegratoImplementazione manuale
Persistenza offlineSupporto sperimentalePacchetti esterni
TracciabilitàTramite DevToolsEventi/Stati espliciti
ComposizioneAutomaticaManuale
Generazione di codiceRaccomandataNon richiesta
Dimensione del teamFlessibileTeam grandi

Raccomandazioni per Contesto

La scelta tra Riverpod e BLoC dipende da diversi fattori contestuali.

Scegliere Riverpod quando:

  • Il team privilegia concisione e produttività
  • Il progetto richiede composizione flessibile dello stato
  • Gli sviluppatori provengono da React o altri framework reattivi
  • Il retry automatico e il recupero degli errori sono importanti
  • La persistenza offline è un requisito

Scegliere BLoC quando:

  • Il team apprezza pattern rigorosi e prevedibili
  • Il progetto richiede tracciabilità completa degli eventi per auditing
  • I junior beneficiano di un'architettura imposta
  • Il debug richiede storia delle transizioni
  • Il settore richiede audit trail (fintech, healthcare)

Fonti

Punti Chiave su Riverpod vs BLoC nel 2026

Riverpod e BLoC affrontano efficacemente le esigenze di gestione dello stato in Flutter. Riverpod 3.x eccelle per ergonomia, gestione automatica degli errori e composizione flessibile. BLoC 9.x primeggia in struttura, prevedibilità e tracciabilità enterprise. Entrambe le soluzioni offrono eccellente testabilità e prestazioni ottimali.

Checklist di Decisione

  • Valutare dimensione ed esperienza del team
  • Considerare la complessità del flusso di dati
  • Analizzare le esigenze di tracciabilità e debug
  • Testare entrambe le soluzioni su un prototipo
  • Verificare la coerenza con l'architettura esistente

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

La scelta migliore resta quella che il team padroneggia e mantiene efficacemente. La coerenza nell'applicazione ha la precedenza sulla scelta della soluzione stessa. Per pattern specifici di Dart come isolates e concurrency, consultare la guida Dart isolates e concurrency.

Sfida del giorno

Sapresti trovare il bug in Flutter?

Uno snippet reale, un bug nascosto, un tentativo al giorno. Senza account per provare.

Anthony Fillion-Maillet

Scritto da

Anthony Fillion-Maillet

Fondatore di SharpSkill

Sviluppatore fullstack da oltre 10 anni. Guida SharpSkill e risponde di tutto ciò che vi viene pubblicato.

Aggiornato il 21 agosto 2026

Tag

#flutter
#riverpod
#bloc
#state management
#dart

Condividi

Articoli correlati