Zarządzanie Stanem w Flutter: Riverpod vs BLoC - Kompletny Przewodnik Porównawczy

Szczegółowe porównanie Riverpod 3.x i BLoC 9.x do zarządzania stanem we Flutterze. Architektura, automatyczne ponawianie, testowalność i przypadki użycia, by wybrać najlepsze rozwiązanie.

Porównanie Riverpod i BLoC w zarządzaniu stanem we Flutterze

Zarządzanie stanem stanowi centralne wyzwanie w rozwoju aplikacji Flutter. Riverpod i BLoC dominują w ekosystemie, każde z nich oferuje odrębną filozofię. Ten przewodnik porównuje oba rozwiązania na konkretnych implementacjach, aby ułatwić wybór odpowiedniego narzędzia w zależności od potrzeb projektu.

Wymagania wstępne

Przewodnik zakłada znajomość Fluttera oraz podstaw zarządzania stanem. Przykłady wykorzystują Riverpod 3.x oraz flutter_bloc 9.x, aktualne stabilne wersje w 2026 roku.

Główne Filozofie Obu Podejść

Riverpod i BLoC rozwiązują ten sam problem przy użyciu przeciwstawnych paradygmatów. Zrozumienie tych różnic koncepcyjnych pozwala wybrać właściwe narzędzie dla danego kontekstu.

Riverpod stosuje podejście deklaratywne i reaktywne. Providery definiują źródła danych, które obserwują widgety. Framework automatycznie zarządza cyklem życia, cache'em oraz zależnościami między providerami. Wersja 3.0 wprowadziła znaczące ulepszenia: automatyczne ponawianie dla nieudanych providerów, wstrzymywanie i wznawianie listenerów w zależności od widoczności widgetu oraz eksperymentalne wsparcie dla trwałości offline.

BLoC (Business Logic Component) wymusza ścisłą architekturę zorientowaną na zdarzenia. Komponenty emitują zdarzenia, Bloc je przetwarza i produkuje nowe stany. To jawne rozdzielenie ułatwia śledzenie przepływu danych. Wersja 9.x wprowadziła EmittableStateStreamableSource dla lepszej elastyczności testowania.

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

Wybór między tymi podejściami zależy od preferencji zespołu i ograniczeń projektu. Przegląd opcji zarządzania stanem, w tym GetX, znajduje się w kompletnym porównaniu zarządzania stanem Flutter na 2026 rok.

Konfiguracja Początkowa

Konfiguracja początkowa ujawnia różnice ergonomiczne między oboma rozwiązaniami. Riverpod stawia na prostotę, BLoC oferuje większą strukturę.

Instalacja Riverpod

Riverpod wymaga jednego pakietu i wrappera w korzeniu aplikacji. Generacja kodu z riverpod_generator jest zalecanym podejściem w wersji 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(),
    );
  }
}

Instalacja BLoC

BLoC wymaga kilku pakietów i bardziej rozbudowanej konfiguracji z BlocProviderami dla każdego używanego Bloca.

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

Konfiguracja BLoC wymaga więcej kodu początkowego, ale od początku czyni zależności jawnymi.

Proste Zarządzanie Stanem: Liczniki i Przełączniki

Proste przypadki ilustrują codzienną ergonomię każdego rozwiązania. Riverpod wyróżnia się zwięzłością, BLoC zachowuje strukturę zorientowaną na zdarzenia.

Licznik z Riverpod (wzorzec Notifier)

Riverpod 3.x deprecjonuje StateProvider i StateNotifierProvider na rzecz zunifikowanego API Notifier. Nowy wzorzec jest czystszy i współpracuje z generacją kodu.

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

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

W prostych przypadkach Riverpod znacząco zmniejsza ilość boilerplate'u. BLoC zyskuje na znaczeniu wraz ze wzrostem złożoności logiki lub gdy istotna jest śledzalność zdarzeń.

Legacy Providery w Riverpod 3.x

StateProvider i StateNotifierProvider nadal działają, ale wymagają importu z flutter_riverpod/legacy.dart. Dla nowego kodu należy używać Notifier lub AsyncNotifier.

Asynchroniczne Zarządzanie Stanem: Wywołania API

Operacje asynchroniczne ujawniają moc każdego rozwiązania. Zarządzanie stanami ładowania, błędu i danych to kluczowe wyzwanie. Riverpod 3.x wprowadza automatyczne ponawianie z wykładniczym wycofywaniem dla nieudanych providerów.

Dane Asynchroniczne z 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]),
      ),
    );
  }
}

Dane Asynchroniczne z 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 oferuje granularną kontrolę nad każdym przejściem stanu. Riverpod automatyzuje więcej dzięki AsyncValue i wbudowanej logice ponawiania.

Gotowy na rozmowy o Flutter?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Zależności między Stanami: Kompozycja i Wstrzykiwanie

Rzeczywiste aplikacje obejmują współzależne stany. Zarządzanie tymi zależnościami znacząco różnicuje oba podejścia.

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

Kompozycja z 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 zarządza zależnościami w sposób deklaratywny. BLoC wymaga ręcznego zarządzania subskrypcjami między Blokami.

Testowalność i Mockowanie

Testowanie stanowi decydujące kryterium dla profesjonalnych projektów. Oba rozwiązania wyróżniają się w tym obszarze przy użyciu różnych podejść. Umiejętności testowania są często weryfikowane podczas rozmów kwalifikacyjnych Flutter.

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

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

Pakiet bloc_test oferuje dedykowaną składnię do testowania sekwencji stanów. Riverpod 3.x dodaje ProviderContainer.test() dla automatycznego zwalniania zasobów oraz NotifierProvider.overrideWithBuild() do mockowania wyłącznie metody build.

Pokrycie Testami

Testowanie wyłącznie przypadków podstawowych jest niewystarczające. Testy muszą obejmować błędy sieci, timeouty, stany graniczne oraz nieoczekiwane przejścia stanów. Kompleksowe praktyki testowania opisano w przewodniku testowania Flutter.

Wydajność i Optymalizacja Rebuildów

Wydajność bezpośrednio wpływa na doświadczenie użytkownika. Oba rozwiązania oferują odrębne mechanizmy optymalizacji.

Optymalizacja z 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);
}

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

Oba rozwiązania oferują granularne optymalizacje. Riverpod z select, BLoC z buildWhen oraz BlocSelector. Zaawansowane techniki wydajnościowe opisano w przewodniku optymalizacji wydajności Flutter.

Praktyczny Przypadek Użycia: Pełna Autentykacja

System autentykacji ilustruje rzeczywiste wzorce dla każdego rozwiązania. Ten przypadek łączy stan trwały, wywołania API i nawigację.

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

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

Obie implementacje obsługują tę samą funkcjonalność w odmiennych stylach. BLoC czyni każde przejście jawnym, Riverpod upraszcza składnię.

Tabela Porównawcza

KryteriumRiverpod 3.xBLoC 9.x
Krzywa uczeniaUmiarkowanaBardziej stroma
BoilerplateMinimalny (z codegen)Znaczący
Type SafetyDoskonałyDoskonały
TestowalnośćDoskonałaDoskonała
Automatyczne ponawianieWbudowaneWymaga ręcznej implementacji
Trwałość offlineEksperymentalne wsparcieZewnętrzne pakiety
Możliwość śledzeniaPrzez DevToolsJawne Eventy/Stany
KompozycjaAutomatycznaRęczna
Generowanie koduZalecaneNiewymagane
Wielkość zespołuElastycznaDuże zespoły

Rekomendacje wg Kontekstu

Wybór między Riverpod i BLoC zależy od kilku czynników kontekstowych.

Wybierz Riverpod, gdy:

  • Zespół ceni zwięzłość i produktywność
  • Projekt wymaga elastycznej kompozycji stanu
  • Programiści wywodzą się z Reacta lub innych frameworków reaktywnych
  • Automatyczne ponawianie i obsługa błędów są istotne
  • Trwałość offline jest wymaganiem

Wybierz BLoC, gdy:

  • Zespół docenia ścisłe, przewidywalne wzorce
  • Projekt wymaga pełnej śledzalności zdarzeń do audytu
  • Junior programiści korzystają z narzuconej architektury
  • Debugowanie wymaga szczegółowej historii przejść
  • Branża wymaga śladu audytowego (fintech, ochrona zdrowia)

Źródła

Co warto zapamiętać o Riverpod vs BLoC w 2026

Riverpod i BLoC skutecznie odpowiadają na potrzeby zarządzania stanem we Flutterze. Riverpod 3.x wyróżnia się ergonomią, automatyczną obsługą błędów i elastyczną kompozycją. BLoC 9.x prowadzi w zakresie struktury, przewidywalności i śledzalności na poziomie enterprise. Oba rozwiązania zapewniają doskonałą testowalność oraz optymalną wydajność.

Lista Kontrolna Decyzji

  • Ocenić wielkość i doświadczenie zespołu
  • Uwzględnić złożoność przepływu danych
  • Przeanalizować potrzeby śledzalności i debugowania
  • Przetestować oba rozwiązania na prototypie
  • Sprawdzić spójność z istniejącą architekturą

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Najlepszym wyborem pozostaje to, które zespół opanuje i utrzymuje skutecznie. Spójność w stosowaniu ma pierwszeństwo przed samym wyborem rozwiązania. Wzorce specyficzne dla Darta, takie jak izolaty i współbieżność, opisano w przewodniku po izolatach i współbieżności Dart.

Wyzwanie dnia

Znajdziesz błąd w Flutter?

Prawdziwy fragment kodu, ukryty błąd, jedna próba dziennie. Bez konta, żeby spróbować.

Anthony Fillion-Maillet

Autor:

Anthony Fillion-Maillet

Założyciel SharpSkill

Programista fullstack od ponad 10 lat. Prowadzi SharpSkill i odpowiada za wszystko, co się tu ukazuje.

Zaktualizowano 21 sierpnia 2026

Tagi

#flutter
#riverpod
#bloc
#state management
#dart

Udostępnij

Powiązane artykuły