# Flutter Durum Yönetimi: Riverpod vs BLoC - Kapsamlı Karşılaştırma Rehberi > Flutter durum yönetimi için Riverpod ve BLoC arasında derinlemesine karşılaştırma. Mimari, performans, test edilebilirlik ve en iyi çözümü seçmek için kullanım senaryoları. - Published: 2026-02-14 - Updated: 2026-04-28 - Author: SharpSkill - Tags: flutter, riverpod, bloc, state management, dart - Reading time: 12 min --- Durum yönetimi, Flutter geliştirmede merkezi bir zorluk oluşturur. Riverpod ve BLoC ekosisteme hakimdir, her biri farklı bir felsefe sunar. Bu rehber, proje ihtiyaçlarına göre seçim yapmaya yardımcı olmak için iki çözümü somut uygulamalar üzerinden karşılaştırır. > **Ön Koşullar** > > Bu rehber Flutter ve durum yönetimi temellerine aşinalık varsayar. Örnekler güncel kararlı sürümler olan Riverpod 2.x ve flutter_bloc 8.x kullanır. ## İki Yaklaşımın Temel Felsefeleri Riverpod ve BLoC aynı problemi karşıt paradigmalarla çözer. Bu kavramsal farkları anlamak, her bağlam için doğru aracı seçmeyi sağlar. **Riverpod**, deklaratif ve reaktif bir yaklaşım benimser. Provider'lar, widget'ların gözlemlediği veri kaynaklarını tanımlar. Framework yaşam döngüsünü, önbelleği ve provider'lar arası bağımlılıkları otomatik yönetir. **BLoC** (Business Logic Component), katı bir olay odaklı mimari dayatır. Bileşenler olay yayınlar, Bloc bunları işler ve yeni durumlar üretir. Bu açık ayrım veri akışı takibini kolaylaştırır. ```dart // riverpod_philosophy.dart // Riverpod: simple declaration, framework handles the rest final counterProvider = StateProvider((ref) => 0); // Usage in a widget class CounterWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { // Reactive read: automatic rebuild if value changes final count = ref.watch(counterProvider); return Text('$count'); } } ``` ```dart // bloc_philosophy.dart // BLoC: explicit events/states separation abstract class CounterEvent {} class IncrementPressed extends CounterEvent {} class CounterBloc extends Bloc { CounterBloc() : super(0) { // Each event has its dedicated handler on((event, emit) => emit(state + 1)); } } // Usage in a widget class CounterWidget extends StatelessWidget { @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, count) => Text('$count'), ); } } ``` Bu yaklaşımlar arasındaki seçim, takım tercihlerine ve proje kısıtlamalarına bağlıdır. ## Başlangıç Yapılandırması Başlangıç yapılandırması, iki çözüm arasındaki ergonomi farklarını ortaya koyar. Riverpod sadeliği önceler, BLoC daha fazla yapı sunar. ### Riverpod Kurulumu Riverpod tek bir paket ve uygulamanın kökünde bir wrapper gerektirir. İsteğe bağlı kod üretimi verimliliği artırır. ```dart // main.dart // 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}); @override Widget build(BuildContext context) { return MaterialApp( home: HomeScreen(), ); } } ``` ### BLoC Kurulumu BLoC birden fazla paket ve kullanılan her Bloc için BlocProvider ile daha detaylı bir yapılandırma gerektirir. ```dart // main.dart // 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}); @override Widget build(BuildContext context) { // MultiBlocProvider for multiple Blocs return MultiBlocProvider( providers: [ BlocProvider(create: (_) => AuthBloc()), BlocProvider(create: (_) => ThemeBloc()), ], child: MaterialApp( home: HomeScreen(), ), ); } } ``` BLoC yapılandırması daha fazla başlangıç kodu gerektirir ancak bağımlılıkları en başından açık hale getirir. ## Basit Durum Yönetimi: Sayaçlar ve Anahtarlar Basit durumlar her çözümün günlük ergonomisini gösterir. Riverpod kısalıkta öne çıkar, BLoC olay odaklı yapısını korur. ### Riverpod ile Sayaç ```dart // counter_riverpod.dart // StateProvider: simple state without complex logic final counterProvider = StateProvider((ref) => 0); class CounterScreen extends ConsumerWidget { const CounterScreen({super.key}); @override 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).state++, child: const Icon(Icons.add), ), ); } } ``` ### BLoC ile Sayaç ```dart // counter_bloc.dart // Typed events for each possible action sealed class CounterEvent {} class CounterIncremented extends CounterEvent {} class CounterDecremented extends CounterEvent {} class CounterReset extends CounterEvent {} // Bloc with handlers for each event class CounterBloc extends Bloc { CounterBloc() : super(0) { on((event, emit) => emit(state + 1)); on((event, emit) => emit(state - 1)); on((event, emit) => emit(0)); } } class CounterScreen extends StatelessWidget { const CounterScreen({super.key}); @override Widget build(BuildContext context) { return Scaffold( body: Center( child: BlocBuilder( builder: (context, count) => Text('Count: $count'), ), ), floatingActionButton: FloatingActionButton( // Event dispatch to modify state onPressed: () => context.read().add(CounterIncremented()), child: const Icon(Icons.add), ), ); } } ``` Basit durumlar için Riverpod boilerplate'i belirgin şekilde azaltır. Mantık karmaşıklaştığında BLoC önem kazanır. > **StateProvider vs StateNotifierProvider** > > StateProvider basit ilkel değerlere uygundur. Karmaşık nesneler veya iş mantığı için StateNotifierProvider veya NotifierProvider daha fazla kontrol sunar. ## Asenkron Durum Yönetimi: API Çağrıları Asenkron işlemler her çözümün gücünü ortaya koyar. Yükleme, hata ve veri durumlarının yönetimi önemli bir zorluktur. ### Riverpod ile Asenkron Veri ```dart // async_riverpod.dart // FutureProvider: automatic loading/error/data management final usersProvider = FutureProvider.autoDispose>((ref) async { final repository = ref.watch(userRepositoryProvider); // autoDispose releases resources when provider is no longer used return repository.fetchUsers(); }); class UsersScreen extends ConsumerWidget { const UsersScreen({super.key}); @override 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]), ), ); } } ``` ### BLoC ile Asenkron Veri ```dart // async_bloc.dart // Explicit states for each loading phase sealed class UsersState {} class UsersInitial extends UsersState {} class UsersLoading extends UsersState {} class UsersLoaded extends UsersState { final List users; UsersLoaded(this.users); } class UsersError extends UsersState { final String message; UsersError(this.message); } // Events to trigger actions sealed class UsersEvent {} class UsersFetchRequested extends UsersEvent {} class UsersRefreshRequested extends UsersEvent {} class UsersBloc extends Bloc { final UserRepository _repository; UsersBloc(this._repository) : super(UsersInitial()) { on(_onFetchRequested); on(_onRefreshRequested); } Future _onFetchRequested( UsersFetchRequested event, Emitter emit, ) async { emit(UsersLoading()); try { final users = await _repository.fetchUsers(); emit(UsersLoaded(users)); } catch (e) { emit(UsersError(e.toString())); } } Future _onRefreshRequested( UsersRefreshRequested event, Emitter 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())); } } } } ``` ```dart // users_screen_bloc.dart // Widget with pattern matching on states class UsersScreen extends StatelessWidget { const UsersScreen({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { return switch (state) { UsersInitial() => const Center( child: ElevatedButton( onPressed: _fetchUsers, child: Text('Load'), ), ), UsersLoading() => const Center(child: CircularProgressIndicator()), UsersError(:final message) => Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('Error: $message'), ElevatedButton( onPressed: () => context .read() .add(UsersFetchRequested()), child: const Text('Retry'), ), ], ), ), UsersLoaded(:final users) => ListView.builder( itemCount: users.length, itemBuilder: (context, index) => UserTile(user: users[index]), ), }; }, ); } void _fetchUsers(BuildContext context) { context.read().add(UsersFetchRequested()); } } ``` BLoC her durum geçişi üzerinde ince kontrol sunar. Riverpod AsyncValue ile daha fazlasını otomatikleştirir. ## Durumlar Arası Bağımlılıklar: Kompozisyon ve Enjeksiyon Gerçek uygulamalar birbirine bağımlı durumlar içerir. Bu bağımlılıkların yönetimi iki yaklaşımı belirgin biçimde ayırır. ### Riverpod ile Kompozisyon ```dart // composition_riverpod.dart // Base provider: configuration final apiClientProvider = Provider((ref) { final baseUrl = ref.watch(environmentProvider).apiUrl; return ApiClient(baseUrl: baseUrl); }); // Dependent provider: repository final productRepositoryProvider = Provider((ref) { // Automatic client injection final client = ref.watch(apiClientProvider); return ProductRepository(client); }); // Provider with parameter: product by ID final productProvider = FutureProvider.autoDispose.family( (ref, productId) async { final repository = ref.watch(productRepositoryProvider); return repository.getProduct(productId); }, ); // Derived provider: filtered products final filteredProductsProvider = Provider>((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}); @override 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), ); } } ``` ### BLoC ile Kompozisyon ```dart // composition_bloc.dart // Repository injected into the Bloc class ProductBloc extends Bloc { final ProductRepository _repository; final CartBloc _cartBloc; late final StreamSubscription _cartSubscription; ProductBloc({ required ProductRepository repository, required CartBloc cartBloc, }) : _repository = repository, _cartBloc = cartBloc, super(ProductInitial()) { on(_onFetchRequested); on(_onAddedToCart); // Listen to cart changes _cartSubscription = _cartBloc.stream.listen((cartState) { // React to cart changes if (cartState is CartUpdated) { add(ProductCartSyncRequested(cartState.items)); } }); } Future _onFetchRequested( ProductFetchRequested event, Emitter 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())); } } @override Future close() { _cartSubscription.cancel(); return super.close(); } } // Configuration with dependency injection class ProductsPage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( create: (context) => ProductBloc( repository: context.read(), cartBloc: context.read(), )..add(ProductFetchRequested()), child: const ProductsView(), ); } } ``` Riverpod bağımlılıkları deklaratif olarak yönetir. BLoC, Bloc'lar arası aboneliklerin manuel yönetimini gerektirir. ## Test Edilebilirlik ve Mocklama Test, profesyonel projeler için belirleyici bir kriterdir. İki çözüm de bu alanda farklı yaklaşımlarla öne çıkar. ### Riverpod ile Testler ```dart // test_riverpod.dart 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; late ProviderContainer container; setUp(() { mockRepository = MockUserRepository(); // Isolated container with override container = ProviderContainer( overrides: [ userRepositoryProvider.overrideWithValue(mockRepository), ], ); }); tearDown(() => container.dispose()); test('returns users from repository', () async { // Arrange final expectedUsers = [User(id: '1', name: 'Test')]; when(() => mockRepository.fetchUsers()) .thenAnswer((_) async => expectedUsers); // 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')); expect( () => container.read(usersProvider.future), throwsException, ); }); }); } ``` ### BLoC ile Testler ```dart // test_bloc.dart 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( '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(), isA().having( (s) => s.users.length, 'users count', 1, ), ], ); blocTest( 'emits [Loading, Error] when fetch fails', build: () { when(() => mockRepository.fetchUsers()) .thenThrow(Exception('Network error')); return UsersBloc(mockRepository); }, act: (bloc) => bloc.add(UsersFetchRequested()), expect: () => [ isA(), isA(), ], ); }); } ``` `bloc_test` paketi durum dizilerini test etmek için özel bir sözdizimi sunar. Riverpod standart Flutter test desenlerini kullanır. > **Test Kapsamı** > > Yalnızca temel senaryoları test etmek yetersizdir. Testler ağ hatalarını, zaman aşımlarını, sınır durumlarını ve beklenmedik durum geçişlerini kapsamalıdır. ## Performans ve Yeniden Yapılandırma Optimizasyonu Performans kullanıcı deneyimini doğrudan etkiler. İki çözüm de farklı optimizasyon mekanizmaları sunar. ### Riverpod ile Optimizasyon ```dart // perf_riverpod.dart // select to rebuild only if targeted value changes class UserNameWidget extends ConsumerWidget { @override 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 final expensiveComputationProvider = Provider((ref) { final input = ref.watch(inputProvider); // Computation automatically cached return performExpensiveComputation(input); }); // autoDispose to release unused resources final searchResultsProvider = FutureProvider.autoDispose .family, String>((ref, 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); }); ``` ### BLoC ile Optimizasyon ```dart // perf_bloc.dart // buildWhen limits rebuilds conditionally class UserNameWidget extends StatelessWidget { @override Widget build(BuildContext context) { return BlocBuilder( // 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 { @override Widget build(BuildContext context) { return BlocSelector( // 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); }, ); } } ``` İki çözüm de ince ayarlı optimizasyonlar sunar. Riverpod `select` ile, BLoC `buildWhen` ve `BlocSelector` ile. ## Pratik Uygulama: Tam Kimlik Doğrulama Kimlik doğrulama sistemi her çözümün gerçek kalıplarını gösterir. Bu durum kalıcı durumu, API çağrılarını ve gezintiyi birleştirir. ### Riverpod ile Kimlik Doğrulama ```dart // auth_riverpod.dart // Authentication state with sealed class sealed class AuthState { const AuthState(); } class AuthInitial extends AuthState { const AuthInitial(); } class AuthLoading extends AuthState { const AuthLoading(); } class AuthAuthenticated extends AuthState { final User user; const AuthAuthenticated(this.user); } class AuthUnauthenticated extends AuthState { final String? error; const AuthUnauthenticated([this.error]); } // Notifier to manage auth state class AuthNotifier extends StateNotifier { final AuthRepository _repository; final SecureStorage _storage; AuthNotifier(this._repository, this._storage) : super(const AuthInitial()) { _checkAuthStatus(); } Future _checkAuthStatus() async { final token = await _storage.getToken(); if (token != null) { try { final user = await _repository.getCurrentUser(token); state = AuthAuthenticated(user); } catch (_) { await _storage.deleteToken(); state = const AuthUnauthenticated(); } } else { state = const AuthUnauthenticated(); } } Future login(String email, String password) async { state = const AuthLoading(); try { final result = await _repository.login(email, password); await _storage.saveToken(result.token); state = AuthAuthenticated(result.user); } catch (e) { state = AuthUnauthenticated(e.toString()); } } Future logout() async { await _storage.deleteToken(); state = const AuthUnauthenticated(); } } // Provider with injected dependencies final authProvider = StateNotifierProvider((ref) { return AuthNotifier( ref.watch(authRepositoryProvider), ref.watch(secureStorageProvider), ); }); // Redirect based on auth state final routerProvider = Provider((ref) { final authState = ref.watch(authProvider); return GoRouter( redirect: (context, state) { final isAuth = authState is AuthAuthenticated; final isAuthRoute = state.matchedLocation.startsWith('/auth'); if (!isAuth && !isAuthRoute) return '/auth/login'; if (isAuth && isAuthRoute) return '/home'; return null; }, routes: [...], ); }); ``` ### BLoC ile Kimlik Doğrulama ```dart // auth_bloc.dart // Exhaustive states for authentication sealed class AuthState { const AuthState(); } class AuthInitial extends AuthState { const AuthInitial(); } class AuthCheckInProgress extends AuthState { const AuthCheckInProgress(); } class AuthLoginInProgress extends AuthState { const AuthLoginInProgress(); } class AuthSuccess extends AuthState { final User user; const AuthSuccess(this.user); } class AuthFailure extends AuthState { final String error; const AuthFailure(this.error); } class AuthLoggedOut extends AuthState { const AuthLoggedOut(); } // Authentication events sealed class AuthEvent { const AuthEvent(); } class AuthCheckRequested extends AuthEvent { const AuthCheckRequested(); } class AuthLoginSubmitted extends AuthEvent { final String email; final String password; const AuthLoginSubmitted(this.email, this.password); } class AuthLogoutRequested extends AuthEvent { const AuthLogoutRequested(); } class AuthBloc extends Bloc { final AuthRepository _repository; final SecureStorage _storage; AuthBloc({ required AuthRepository repository, required SecureStorage storage, }) : _repository = repository, _storage = storage, super(const AuthInitial()) { on(_onCheckRequested); on(_onLoginSubmitted); on(_onLogoutRequested); } Future _onCheckRequested( AuthCheckRequested event, Emitter 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 _onLoginSubmitted( AuthLoginSubmitted event, Emitter 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 _onLogoutRequested( AuthLogoutRequested event, Emitter emit, ) async { await _storage.deleteToken(); emit(const AuthLoggedOut()); } } ``` İki uygulama da aynı işlevselliği farklı tarzlarda ele alır. BLoC her geçişi açıkça ifade eder, Riverpod sözdizimini sadeleştirir. ## Karşılaştırmalı Özet Tablo | Kriter | Riverpod | BLoC | |--------|----------|------| | **Öğrenme eğrisi** | Orta | Daha dik | | **Boilerplate** | Minimum | Belirgin | | **Tip Güvenliği** | Mükemmel | Mükemmel | | **Test Edilebilirlik** | Mükemmel | Mükemmel | | **Takip Edilebilirlik** | DevTools üzerinden | Açık Olay/Durum | | **Kompozisyon** | Otomatik | Manuel | | **Kod Üretimi** | İsteğe bağlı | Gerekli değil | | **Takım Boyutu** | Esnek | Büyük takımlar | ## Bağlama Göre Öneriler Riverpod ve BLoC arasındaki seçim çeşitli bağlamsal etkenlere bağlıdır. **Riverpod'u şu durumlarda seç:** - Takım kısalık ve verimliliği önceler - Proje esnek durum kompozisyonu gerektirir - Geliştiriciler React veya başka reaktif framework'lerden gelir - Otomatik önbellekleme önemli bir avantaj sunar **BLoC'u şu durumlarda seç:** - Takım katı ve öngörülebilir kalıpları benimser - Proje tam olay takibi gerektirir - Kıdemsiz üyeler dayatılmış mimariden faydalanır - Hata ayıklama geçiş geçmişine ihtiyaç duyar ## Sonuç Riverpod ve BLoC, Flutter'da durum yönetimi ihtiyaçlarını etkili biçimde karşılar. Riverpod ergonomi ve esneklikte öne çıkar, BLoC yapı ve öngörülebilirlikte. İki çözüm de mükemmel test edilebilirlik ve optimum performans sunar. ### Karar Kontrol Listesi - ✅ Takım büyüklüğünü ve deneyimini değerlendir - ✅ Veri akışı karmaşıklığını dikkate al - ✅ Takip edilebilirlik ve hata ayıklama gereksinimlerini analiz et - ✅ İki çözümü bir prototipte test et - ✅ Mevcut mimariyle tutarlılığı doğrula En iyi seçim, takımın ustalaştığı ve etkili biçimde sürdürdüğü çözümdür. Uygulamadaki tutarlılık, çözümün kendisinin seçiminden önce gelir. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/tr/blog/flutter/flutter-state-management-riverpod-vs-bloc