State Management Flutter : Riverpod vs BLoC - Guide Comparatif Complet

Comparaison approfondie entre Riverpod 3.x et BLoC 9.x pour la gestion d'état Flutter. Architecture, retry automatique, testabilité et cas d'usage pour choisir la meilleure solution.

Comparaison Riverpod et BLoC pour le state management Flutter

La gestion d'état représente un défi central dans le développement Flutter. Riverpod et BLoC dominent l'écosystème, chacun proposant une philosophie distincte. Ce guide compare ces deux solutions à travers des implémentations concrètes pour éclairer le choix selon les besoins du projet.

Prérequis

Ce guide suppose une familiarité avec Flutter et les bases du state management. Les exemples utilisent Riverpod 3.x et flutter_bloc 9.x, les versions stables actuelles en 2026.

Philosophies fondamentales des deux approches

Riverpod et BLoC répondent au même problème avec des paradigmes opposés. Comprendre ces différences conceptuelles permet de choisir l'outil adapté à chaque contexte.

Riverpod adopte une approche déclarative et réactive. Les providers définissent des sources de données que les widgets observent. Le framework gère automatiquement le cycle de vie, la mise en cache et les dépendances entre providers. La version 3.0 apporte des améliorations significatives : retry automatique pour les providers échoués, pause/reprise des listeners selon la visibilité du widget, et persistance hors ligne expérimentale.

BLoC (Business Logic Component) impose une architecture événementielle stricte. Les composants émettent des événements, le Bloc les traite et produit de nouveaux états. Cette séparation explicite facilite le suivi du flux de données. La version 9.x introduit EmittableStateStreamableSource pour une flexibilité accrue dans les tests.

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'),
    );
  }
}

Le choix entre ces approches dépend des préférences d'équipe et des contraintes du projet. Pour une vue d'ensemble des options de gestion d'état incluant GetX, consultez la comparaison complète du state management Flutter pour 2026.

Configuration et mise en place initiale

La configuration initiale révèle les différences d'ergonomie entre les deux solutions. Riverpod privilégie la simplicité, BLoC offre plus de structure.

Installation de Riverpod

Riverpod nécessite un seul package et un wrapper à la racine de l'application. La génération de code avec riverpod_generator est l'approche recommandée en version 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(),
    );
  }
}

Installation de BLoC

BLoC demande plusieurs packages et une configuration plus élaborée avec des BlocProviders pour chaque Bloc utilisé.

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 configuration BLoC exige plus de code initial mais rend les dépendances explicites dès le départ.

Gestion d'état simple : compteurs et toggles

Les cas simples illustrent l'ergonomie quotidienne de chaque solution. Riverpod excelle dans la concision, BLoC maintient sa structure événementielle.

Compteur avec Riverpod (Pattern Notifier)

Riverpod 3.x déprécie StateProvider et StateNotifierProvider au profit de l'API unifiée Notifier. Le nouveau pattern est plus propre et fonctionne parfaitement avec la génération de code.

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),
      ),
    );
  }
}

Compteur avec 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),
      ),
    );
  }
}

Pour les cas simples, Riverpod réduit significativement le boilerplate. BLoC devient pertinent quand la logique se complexifie ou quand la traçabilité des événements importe.

Providers legacy dans Riverpod 3.x

StateProvider et StateNotifierProvider fonctionnent toujours mais nécessitent l'import depuis flutter_riverpod/legacy.dart. Pour le nouveau code, utilisez Notifier ou AsyncNotifier.

Gestion d'état asynchrone : appels API

Les opérations asynchrones révèlent la puissance de chaque solution. La gestion des états loading/error/data constitue un enjeu majeur. Riverpod 3.x introduit le retry automatique avec backoff exponentiel pour les providers échoués.

Données asynchrones avec 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('Erreur: $error'),
            ElevatedButton(
              // invalidate forces reload
              onPressed: () => ref.invalidate(usersProvider),
              child: const Text('Réessayer'),
            ),
          ],
        ),
      ),
      data: (users) => ListView.builder(
        itemCount: users.length,
        itemBuilder: (context, index) => UserTile(user: users[index]),
      ),
    );
  }
}

Données asynchrones avec 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('Charger'),
              ),
            ),
          UsersLoading() => const Center(child: CircularProgressIndicator()),
          UsersError(:final message) => Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text('Erreur: $message'),
                  ElevatedButton(
                    onPressed: () => context
                        .read<UsersBloc>()
                        .add(UsersFetchRequested()),
                    child: const Text('Réessayer'),
                  ),
                ],
              ),
            ),
          UsersLoaded(:final users) => ListView.builder(
              itemCount: users.length,
              itemBuilder: (context, index) => UserTile(user: users[index]),
            ),
        };
      },
    );
  }
}

BLoC offre un contrôle granulaire sur chaque transition d'état. Riverpod automatise davantage avec AsyncValue et la logique de retry intégrée.

Prêt à réussir tes entretiens Flutter ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Dépendances entre états : composition et injection

Les applications réelles impliquent des états interdépendants. La gestion de ces dépendances différencie significativement les deux approches.

Composition avec 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),
    );
  }
}

Composition avec 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 gère les dépendances de manière déclarative. BLoC requiert une gestion manuelle des subscriptions entre Blocs.

Testabilité et mocking

Les tests constituent un critère décisif pour les projets professionnels. Les deux solutions excellent dans ce domaine avec des approches différentes. Les compétences en testing sont fréquemment évaluées lors des entretiens Flutter.

Tests avec 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,
      );
    });
  });
}

Tests avec 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>(),
      ],
    );
  });
}

Le package bloc_test offre une syntaxe dédiée pour tester les séquences d'états. Riverpod 3.x ajoute ProviderContainer.test() pour le dispose automatique et NotifierProvider.overrideWithBuild() pour mocker uniquement la méthode build.

Couverture des tests

Tester uniquement les cas nominaux ne suffit pas. Les tests doivent couvrir les erreurs réseau, les timeouts, les états edge-case et les transitions d'état inattendues. Pour des pratiques de test complètes, consultez le guide du testing Flutter.

Performances et optimisation des rebuilds

Les performances impactent directement l'expérience utilisateur. Les deux solutions proposent des mécanismes d'optimisation distincts.

Optimisation 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);
}

Optimisation 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);
      },
    );
  }
}

Les deux solutions offrent des optimisations granulaires. Riverpod avec select, BLoC avec buildWhen et BlocSelector. Pour des techniques de performance avancées, consultez le guide d'optimisation des performances Flutter.

Cas d'usage pratique : authentification complète

Un système d'authentification illustre les patterns réels de chaque solution. Ce cas combine état persistant, appels API et navigation.

Authentification avec 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: [...],
  );
}

Authentification avec 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());
  }
}

Les deux implémentations gèrent les mêmes fonctionnalités avec des styles différents. BLoC rend chaque transition explicite, Riverpod simplifie la syntaxe.

Tableau comparatif récapitulatif

CritèreRiverpod 3.xBLoC 9.x
Courbe d'apprentissageModéréePlus prononcée
BoilerplateMinimal (avec codegen)Significatif
TypageExcellentExcellent
TestabilitéExcellenteExcellente
Retry automatiqueIntégréImplémentation manuelle
Persistance hors ligneSupport expérimentalPackages externes
TraçabilitéVia DevToolsEvents/States explicites
CompositionAutomatiqueManuelle
Code generationRecommandéeNon requise
Taille équipeFlexibleGrandes équipes

Recommandations selon le contexte

Le choix entre Riverpod et BLoC dépend de plusieurs facteurs contextuels.

Choisir Riverpod quand :

  • L'équipe privilégie la concision et la productivité
  • Le projet nécessite une composition flexible d'états
  • Les développeurs viennent de React ou d'autres frameworks réactifs
  • Le retry automatique et la récupération d'erreur importent
  • La persistance hors ligne est un besoin

Choisir BLoC quand :

  • L'équipe apprécie les patterns stricts et prévisibles
  • Le projet requiert une traçabilité complète des événements pour audit
  • Les juniors bénéficient d'une architecture imposée
  • Le debugging nécessite un historique des transitions détaillé
  • L'industrie exige des pistes d'audit (fintech, santé)

Sources

Ce qu'il faut retenir de Riverpod vs BLoC en 2026

Riverpod et BLoC répondent efficacement aux besoins de gestion d'état Flutter. Riverpod 3.x excelle en ergonomie, gestion automatique des erreurs et composition flexible. BLoC 9.x se distingue par sa structure, sa prévisibilité et sa traçabilité adaptée aux projets d'entreprise. Les deux solutions offrent une testabilité excellente et des performances optimales.

Checklist de décision

  • Évaluer la taille et l'expérience de l'équipe
  • Considérer la complexité du flux de données
  • Analyser les besoins en traçabilité et debugging
  • Tester les deux solutions sur un prototype
  • Vérifier la cohérence avec l'architecture existante

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Le meilleur choix reste celui que l'équipe maîtrise et maintient efficacement. La consistance dans l'application prime sur le choix de la solution elle-même. Pour les patterns spécifiques à Dart comme les isolates et la concurrence, consultez le guide des isolates et de la concurrence Dart.

Défi du jour

Tu saurais repérer le bug en Flutter ?

Un vrai bout de code, un bug caché, une tentative par jour. Sans compte pour essayer.

Anthony Fillion-Maillet

Écrit par

Anthony Fillion-Maillet

Fondateur de SharpSkill

Développeur fullstack depuis plus de 10 ans. Il dirige SharpSkill et répond de tout ce qui y est publié.

Mis à jour le 21 août 2026

Tags

#flutter
#riverpod
#bloc
#state management
#dart

Partager

Articles similaires