Flutter State Management: Riverpod vs BLoC - Volledige Vergelijkingsgids

Diepgaande vergelijking tussen Riverpod 3.x en BLoC 9.x voor state management in Flutter. Architectuur, automatische retry, testbaarheid en use cases om de beste oplossing te kiezen.

Vergelijking tussen Riverpod en BLoC voor Flutter state management

State management vormt een centrale uitdaging in Flutter-ontwikkeling. Riverpod en BLoC domineren het ecosysteem, elk met een eigen filosofie. Deze gids vergelijkt beide oplossingen aan de hand van concrete implementaties om de keuze te onderbouwen op basis van de projectbehoeften.

Vereisten

Deze gids veronderstelt vertrouwdheid met Flutter en de basisprincipes van state management. De voorbeelden gebruiken Riverpod 3.x en flutter_bloc 9.x, de huidige stabiele versies in 2026.

Kernfilosofieën van Beide Benaderingen

Riverpod en BLoC lossen hetzelfde probleem op met tegenovergestelde paradigma's. Het begrijpen van deze conceptuele verschillen maakt het mogelijk om de juiste tool voor elke context te kiezen.

Riverpod hanteert een declaratieve en reactieve aanpak. Providers definiëren databronnen die widgets observeren. Het framework beheert automatisch de levenscyclus, caching en afhankelijkheden tussen providers. Versie 3.0 bracht belangrijke verbeteringen: automatische retry voor gefaalde providers, pause/resume van listeners op basis van widget-zichtbaarheid en experimentele offline-persistentie.

BLoC (Business Logic Component) dwingt een strikte event-driven architectuur af. Componenten zenden events uit, de Bloc verwerkt ze en produceert nieuwe states. Deze expliciete scheiding vergemakkelijkt het traceren van de datastroom. Versie 9.x introduceerde EmittableStateStreamableSource voor verbeterde test-flexibiliteit.

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

De keuze tussen deze benaderingen hangt af van teamvoorkeuren en projectbeperkingen. Voor een overzicht van state management opties inclusief GetX, zie de volledige Flutter state management vergelijking voor 2026.

Initiële Configuratie

De initiële configuratie onthult ergonomische verschillen tussen beide oplossingen. Riverpod geeft prioriteit aan eenvoud, BLoC biedt meer structuur.

Installatie van Riverpod

Riverpod vereist één enkel pakket en een wrapper aan de root van de applicatie. Code generation met riverpod_generator is de aanbevolen aanpak in versie 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(),
    );
  }
}

Installatie van BLoC

BLoC vereist meerdere pakketten en een uitgebreidere configuratie met BlocProviders voor elke gebruikte Bloc.

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

De BLoC-configuratie vraagt meer initiële code maar maakt afhankelijkheden vanaf het begin expliciet.

Eenvoudig State Management: Counters en Toggles

Eenvoudige cases illustreren de dagelijkse ergonomie van elke oplossing. Riverpod blinkt uit in beknoptheid, BLoC behoudt zijn event-driven structuur.

Counter met Riverpod (Notifier Pattern)

Riverpod 3.x depreceert StateProvider en StateNotifierProvider ten gunste van de uniforme Notifier API. Het nieuwe pattern is overzichtelijker en werkt naadloos met code generation.

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

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

Voor eenvoudige cases reduceert Riverpod de boilerplate aanzienlijk. BLoC wordt relevant zodra de logica complexer wordt of wanneer event-traceerbaarheid belangrijk is.

Legacy Providers in Riverpod 3.x

StateProvider en StateNotifierProvider werken nog steeds maar vereisen import uit flutter_riverpod/legacy.dart. Voor nieuwe code, gebruik Notifier of AsyncNotifier.

Asynchroon State Management: API-aanroepen

Asynchrone operaties tonen de kracht van elke oplossing. Het beheren van loading-, error- en data-states vormt een belangrijke uitdaging. Riverpod 3.x introduceert automatische retry met exponentiële backoff voor gefaalde providers.

Asynchrone Data met 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]),
      ),
    );
  }
}

Asynchrone Data met 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 biedt granulaire controle over elke state-overgang. Riverpod automatiseert meer via AsyncValue en ingebouwde retry-logica.

Klaar om je Flutter gesprekken te halen?

Oefen met onze interactieve simulatoren, flashcards en technische tests.

State-afhankelijkheden: Compositie en Injectie

Echte applicaties bevatten onderling afhankelijke states. Het beheer van deze afhankelijkheden onderscheidt beide benaderingen aanzienlijk.

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

Compositie met 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 beheert afhankelijkheden declaratief. BLoC vereist handmatig beheer van subscriptions tussen Blocs.

Testbaarheid en Mocking

Testing vormt een doorslaggevend criterium voor professionele projecten. Beide oplossingen blinken uit op dit gebied met verschillende benaderingen. Testvaardigheden worden regelmatig geëvalueerd in Flutter interviews.

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

Testen met 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>(),
      ],
    );
  });
}

Het bloc_test-pakket biedt toegewijde syntax voor het testen van state-sequenties. Riverpod 3.x voegt ProviderContainer.test() toe voor automatische opruiming en NotifierProvider.overrideWithBuild() voor het mocken van alleen de build-methode.

Testdekking

Alleen de standaardgevallen testen is onvoldoende. Tests moeten netwerkfouten, timeouts, edge-case states en onverwachte state-overgangen dekken. Voor uitgebreide testpraktijken, zie de Flutter testing gids.

Prestaties en Rebuild-optimalisatie

Prestaties hebben directe invloed op de gebruikerservaring. Beide oplossingen bieden verschillende optimalisatiemechanismen.

Optimalisatie met 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);
}

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

Beide oplossingen bieden granulaire optimalisaties. Riverpod met select, BLoC met buildWhen en BlocSelector. Voor geavanceerde prestatie-optimalisatietechnieken, zie de Flutter prestatie-optimalisatie gids.

Praktische Use Case: Volledige Authenticatie

Een authenticatiesysteem illustreert de echte patronen voor elke oplossing. Deze case combineert persistente state, API-aanroepen en navigatie.

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

Authenticatie met 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());
  }
}

Beide implementaties dekken dezelfde functionaliteit met verschillende stijlen. BLoC maakt elke overgang expliciet, Riverpod vereenvoudigt de syntax.

Vergelijkingstabel Samenvatting

CriteriumRiverpod 3.xBLoC 9.x
LeercurveGemiddeldSteiler
BoilerplateMinimaal (met codegen)Aanzienlijk
Type SafetyUitstekendUitstekend
TestbaarheidUitstekendUitstekend
Automatische RetryIngebouwdHandmatige implementatie
Offline PersistentieExperimentele ondersteuningExterne pakketten
TraceerbaarheidVia DevToolsExpliciete Events/States
CompositieAutomatischHandmatig
Code generationAanbevolenNiet vereist
TeamgrootteFlexibelGrote teams

Aanbevelingen per Context

De keuze tussen Riverpod en BLoC hangt af van verschillende contextuele factoren.

Kies Riverpod wanneer:

  • Het team beknoptheid en productiviteit prioriteert
  • Het project flexibele state-compositie vereist
  • Ontwikkelaars uit React of andere reactieve frameworks komen
  • Automatische retry en foutherstel belangrijk zijn
  • Offline persistentie een vereiste is

Kies BLoC wanneer:

  • Het team strikte, voorspelbare patterns waardeert
  • Het project volledige event-traceerbaarheid voor auditing vereist
  • Junioren profiteren van een opgelegde architectuur
  • Debugging een geschiedenis van overgangen vereist
  • De sector audit trails vereist (fintech, healthcare)

Bronnen

Kernpunten over Riverpod vs BLoC in 2026

Riverpod en BLoC voorzien effectief in de behoeften van state management in Flutter. Riverpod 3.x blinkt uit in ergonomie, automatische foutafhandeling en flexibele compositie. BLoC 9.x excelleert in structuur, voorspelbaarheid en enterprise-niveau traceerbaarheid. Beide oplossingen bieden uitstekende testbaarheid en optimale prestaties.

Beslissingschecklist

  • Evalueer de teamgrootte en ervaring
  • Overweeg de complexiteit van de datastroom
  • Analyseer de behoeften aan traceerbaarheid en debugging
  • Test beide oplossingen op een prototype
  • Verifieer de consistentie met de bestaande architectuur

Begin met oefenen!

Test je kennis met onze gespreksimulatoren en technische tests.

De beste keuze blijft die welke het team beheerst en effectief onderhoudt. Consistentie in de toepassing heeft voorrang op de keuze van de oplossing zelf. Voor Dart-specifieke patterns zoals isolates en concurrency, zie de Dart isolates en concurrency gids.

Dagelijkse challenge

Zie jij de bug in Flutter?

Een echt codefragment, een verborgen bug, één poging per dag. Zonder account uit te proberen.

Anthony Fillion-Maillet

Geschreven door

Anthony Fillion-Maillet

Oprichter van SharpSkill

Al meer dan 10 jaar fullstack-ontwikkelaar. Hij leidt SharpSkill en staat in voor alles wat hier verschijnt.

Bijgewerkt op 21 augustus 2026

Tags

#flutter
#riverpod
#bloc
#state management
#dart

Delen

Gerelateerde artikelen