Flutter State Management: Riverpod, Provider ve Bloc Karşılaştırması
# Flutter State Management Rehberi: Riverpod, Provider ve Bloc Karşılaştırması
State management seçimi, Flutter projelerinde ölçeklenebilirliği, test edilebilirliği ve geliştirme hızını doğrudan etkiler. Üç büyük çözümü de üretim projelerinde kullandıktan sonra, yüzeysel karşılaştırmaların ötesine geçen kapsamlı bir rehber hazırlamak istedim.
State Management Neden Bu Kadar Önemli?
Flutter'da her widget, state'i değiştiğinde yeniden çizilir. Küçük bir uygulamada `setState()` gayet yeterlidir. Ancak uygulama büyüdükçe ciddi sorunlarla karşılaşırsınız: tekrarlanan iş mantığı, öngörülemeyen rebuild'ler, birbirine sıkı bağlı widget'lar ve yazmaktan kaçındığınız testler. Doğru bir state management çözümü tüm bu sorunları ortadan kaldırır.
Üç Büyük Aday
Provider
Provider, Flutter ekibinin resmi olarak önerdiği ilk topluluk çözümüydü. `InheritedWidget` mekanizmasını geliştirici dostu bir API ile sarar ve basit uygulamalar için hâlâ sağlam bir tercihtir.
class=class="code-string">"code-comment">// Provider ile basit bir sayaç tanımlama
class CounterModel extends ChangeNotifier {
int _count = class="code-number">0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
class=class="code-string">"code-comment">// Providerclass="code-string">'ı widget ağacının en üstünde kaydetme
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CounterModel(),
child: const MyApp(),
),
);
}
class=class="code-string">"code-comment">// Widget içinde state'i tüketme
class CounterPage extends StatelessWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context) {
final counter = context.watch<CounterModel>();
return Scaffold(
body: Center(child: Text(class="code-string">'Sayaç: ${counter.count}')),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read<CounterModel>().increment(),
child: const Icon(Icons.add),
),
);
}
}Güçlü yönleri: Minimum boilerplate, kolay öğrenme eğrisi, Flutter ekibinin kapsamlı dokümantasyonu.
Zayıf yönleri: `BuildContext`'e bağımlılık, provider'ları birleştirme zorluğu, sınırlı derleme zamanı güvenliği.
Riverpod
Riverpod, Provider'ın aynı yazar tarafından geliştirilen ruhani halefidir. `BuildContext` bağımlılığını ortadan kaldırır, derleme zamanı güvenliği sağlar ve provider'ları birleştirmek ile override etmek için çok daha zengin bir API sunar.
class=class="code-string">"code-comment">// Riverpod ile basit bir sayaç tanımlama
final counterProvider = NotifierProvider<CounterNotifier, int>(
CounterNotifier.new,
);
class CounterNotifier extends Notifier<int> {
@override
int build() => class="code-number">0;
void increment() {
state++;
}
}
class=class="code-string">"code-comment">// Kök widgetclass="code-string">'ta sarmalayıcıya gerek yok—sadece ProviderScope kullanın
void main() {
runApp(
const ProviderScope(child: MyApp()),
);
}
class=class="code-string">"code-comment">// Widget içinde state'i tüketme
class CounterPage extends ConsumerWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Scaffold(
body: Center(child: Text(class="code-string">'Sayaç: $count')),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(counterProvider.notifier).increment(),
child: const Icon(Icons.add),
),
);
}
}Güçlü yönleri: `BuildContext` bağımlılığı yok, derleme zamanı provider güvenliği, mükemmel test desteği, yerleşik önbellekleme ve otomatik temizlik.
Zayıf yönleri: Provider'a göre daha dik öğrenme eğrisi, küçük projeler için kod üretimi kurulumu ağır gelebilir.
Bloc
Bloc, katı bir olay güdümlü desen uygular: widget'lar olay gönderir, Bloc bunları işler ve yeni state'ler yayar. Bu tek yönlü akış son derece öngörülebilirdir ve tutarlılığın geliştirme hızından daha önemli olduğu büyük ekiplerde iyi ölçeklenir.
class=class="code-string">"code-comment">// Eventclass="code-string">'ler
sealed class CounterEvent {}
class CounterIncremented extends CounterEvent {}
class=class="code-string">"code-comment">// State bu basit örnekte sadece bir int
class=class="code-string">"code-comment">// Bloc tanımı
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(class="code-number">0) {
on<CounterIncremented>((event, emit) {
emit(state + class="code-number">1);
});
}
}
class=class="code-string">"code-comment">// flutter_bloc ile sağlama ve tüketme
void main() {
runApp(
BlocProvider(
create: (_) => CounterBloc(),
child: const MyApp(),
),
);
}
class CounterPage extends StatelessWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: BlocBuilder<CounterBloc, int>(
builder: (context, count) => Text('Sayaç: $count'),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () =>
context.read<CounterBloc>().add(CounterIncremented()),
child: const Icon(Icons.add),
),
);
}
}Güçlü yönleri: Yüksek düzeyde öngörülebilir tek yönlü akış, mükemmel DevTools desteği, her state geçişi event'ler aracılığıyla izlenebilir.
Zayıf yönleri: Ciddi miktarda boilerplate (event, state, bloc sınıfları), basit özellikler için gereğinden fazla karmaşık.
Aynı Özellik, Üç Farklı Yaklaşım: Asenkron Veri Çekme
Sayaç önemsiz bir örnek. Gerçek bir senaryoya bakalım: API'den kullanıcı profili çekme.
Provider Yaklaşımı
class UserProfileModel extends ChangeNotifier {
UserProfile? _profile;
bool _loading = false;
String? _error;
UserProfile? get profile => _profile;
bool get loading => _loading;
String? get error => _error;
Future<void> fetchProfile(String userId) async {
_loading = true;
_error = null;
notifyListeners();
try {
_profile = await ApiService().getUser(userId);
} catch (e) {
_error = e.toString();
} finally {
_loading = false;
notifyListeners();
}
}
}Riverpod Yaklaşımı
final userProfileProvider =
FutureProvider.autoDispose.family<UserProfile, String>((ref, userId) {
return ref.watch(apiServiceProvider).getUser(userId);
});
class=class="code-string">"code-comment">// Widgetclass="code-string">'ta yükleme, hata ve veri durumları otomatik olarak ele alınır
class UserProfilePage extends ConsumerWidget {
final String userId;
const UserProfilePage({super.key, required this.userId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final profileAsync = ref.watch(userProfileProvider(userId));
return profileAsync.when(
loading: () => const CircularProgressIndicator(),
error: (err, stack) => Text('Hata: $err'),
data: (profile) => Text(profile.name),
);
}
}Bloc Yaklaşımı
class=class="code-string">"code-comment">// Eventclass="code-string">'ler
sealed class UserProfileEvent {}
class UserProfileRequested extends UserProfileEvent {
final String userId;
UserProfileRequested(this.userId);
}
class=class="code-string">"code-comment">// State'ler
sealed class UserProfileState {}
class UserProfileInitial extends UserProfileState {}
class UserProfileLoading extends UserProfileState {}
class UserProfileLoaded extends UserProfileState {
final UserProfile profile;
UserProfileLoaded(this.profile);
}
class UserProfileError extends UserProfileState {
final String message;
UserProfileError(this.message);
}
class=class="code-string">"code-comment">// Bloc
class UserProfileBloc extends Bloc<UserProfileEvent, UserProfileState> {
final ApiService api;
UserProfileBloc(this.api) : super(UserProfileInitial()) {
on<UserProfileRequested>((event, emit) async {
emit(UserProfileLoading());
try {
final profile = await api.getUser(event.userId);
emit(UserProfileLoaded(profile));
} catch (e) {
emit(UserProfileError(e.toString()));
}
});
}
}Riverpod'un asenkron durumları neredeyse sıfır boilerplate ile ele aldığına dikkat edin. Bloc, açıkça tanımlanmış event ve state sınıfları gerektirir. Provider ise ikisinin arasında kalır ve yükleme/hata takibini manuel olarak yapmanızı ister.
Detaylı Karşılaştırma Tablosu
| Kriter | Provider | Riverpod | Bloc |
|---|---|---|---|
| Boilerplate | Düşük | Orta | Yüksek |
| Öğrenme eğrisi | Kolay | Orta | Dik |
| Derleme zamanı güvenliği | Sınırlı | Güçlü | Orta |
| Test edilebilirlik | İyi | Mükemmel | Mükemmel |
| Ölçeklenebilirlik | Orta | Yüksek | Yüksek |
| DevTools desteği | Temel | İyi | Mükemmel |
| Asenkron işleme | Manuel | Yerleşik | Manuel (olay güdümlü) |
| BuildContext bağımlılığı | Var | Yok | Var (sağlama için) |
| Topluluk büyüklüğü | Geniş | Hızla büyüyor | Geniş |
| Kod üretimi | Gerekmez | Opsiyonel ama önerilir | Gerekmez |
Geçiş Planı: Provider'dan Riverpod'a
Mevcut bir Provider tabanlı uygulamanız varsa ve Riverpod'a geçmek istiyorsanız, riskli bir toptan yeniden yazımdan kaçınan aşamalı bir yaklaşım önerebilirim.
Aşama 1: Riverpod'u Provider'ın yanına ekleyin
Uygulama kökünüzü hem `ProviderScope` hem de mevcut `MultiProvider` ile sarın. İkisi çakışmadan bir arada çalışır.
void main() {
runApp(
ProviderScope(
child: MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => AuthModel()),
class=class="code-string">"code-comment">// ... mevcut provider'lar
],
child: const MyApp(),
),
),
);
}Aşama 2: Önce uç özellikleri taşıyın
Diğer provider'lara fazla bağımlı olmayan bağımsız ekran veya özelliklerle başlayın. `ChangeNotifier` sınıflarını Riverpod notifier'larına dönüştürün, widget'lardaki `context.watch` çağrılarını `ref.watch` ile değiştirin.
Aşama 3: Paylaşılan servisleri taşıyın
Kimlik doğrulama, API istemcileri ve diğer paylaşılan servisleri Riverpod provider'larına taşıyın. Riverpod provider'ları global olduğu ve `BuildContext` gerektirmediği için bu adım genellikle kodu sadeleştirir.
Aşama 4: Provider'ı kaldırın
Tüm özellikler taşındıktan sonra, kökteki `MultiProvider`'ı kaldırın ve `pubspec.yaml`'dan `provider` paketini çıkarın.
Sık Yapılan State Management Hataları
1. Her şeyi global state'e koymak
Her veri parçasının bir provider veya bloc'a ait olması gerekmez. Form alanı görünürlüğü veya animasyon ilerlemesi gibi yalnızca UI ile ilgili durumlar için `setState` veya `ValueNotifier` ile yerel widget state'i gayet yeterlidir.
2. Gereksiz widget yeniden çizimleri
Sadece tek bir alana ihtiyacınız varken modelin tamamını izlemek, gereksiz rebuild'lere neden olur. Riverpod'da `select`, Bloc'ta `BlocSelector` kullanarak yalnızca belirli dilimleri dinleyin.
class=class="code-string">"code-comment">// Kötü: kullanıcı modelindeki herhangi bir değişiklikte yeniden çizer
final user = ref.watch(userProvider);
class=class="code-string">"code-comment">// İyi: sadece isim değiştiğinde yeniden çizer
final name = ref.watch(userProvider.select((u) => u.name));3. Widget'lara iş mantığı koymak
Widget'lar state okumalı ve aksiyon tetiklemeli, mantık içermemelidir. `build` metodunuz hangi API'yi çağıracağına `if/else` zincirleriyle karar veriyorsa, bu mantık bir notifier veya bloc'a taşınmalıdır.
4. Temizlemeyi ihmal etmek
Controller'ları, stream'leri veya subscription'ları dispose etmeyi unutmak bellek sızıntılarına yol açar. Riverpod'un `autoDispose` özelliği bunu zarif bir şekilde halleder; Provider ve Bloc'ta temizlik konusunda disiplinli olmanız gerekir.
5. Proje ihtiyacı yerine popülerliğe göre seçmek
En iyi state management çözümü, ekibinizin anlayıp sürdürebileceği çözümdür. İyi uygulanmış bir Provider kod tabanı, kötü anlaşılmış bir Bloc mimarisini her zaman yener.
Kişisel Deneyimlerim
Projelerimde, geliştirdiğim uygulamaların büyük çoğunluğu için Riverpod'un en verimli seçenek olduğunu gördüm. Derleme zamanı güvenliği, provider bağımlılık hatalarını çalışma zamanına ulaşmadan yakalar ve `FutureProvider` ile `AsyncValue` ikilisi, API odaklı ekranlarda o kadar çok boilerplate'i ortadan kaldırır ki sanki farklı bir framework kullanıyormuşsunuz gibi hissettiriyor. Özellikle bir Riverpod provider'ını test etmek için widget ağacı oluşturmanın gerekmemesini çok değerli buluyorum.
Öte yandan, her geliştiricinin aynı katı kalıbı izlemesi gereken büyük ekiplerle çalıştığım projelerde Bloc'u tercih ediyorum. Tek başınıza geliştirirken boilerplate gibi hissettiren event/state seremonisi, aynı özelliğe beş kişi dokunduğunda değerli bir iletişim protokolüne dönüşüyor.
Hız ve hızlı iterasyonun uzun vadeli mimariden daha önemli olduğu prototip ve MVP'ler için hâlâ Provider kullanıyorum. Riverpod'a geçiş yolu yeterince pürüzsüz olduğundan, Provider ile başlamak çıkmaz bir sokak değil.
Sonuç
2026'daki yeni Flutter projelerin çoğu için Riverpod, tür güvenliği, test edilebilirlik ve geliştirici deneyimi arasında en güçlü dengeyi sunuyor. Provider, küçük uygulamalar için pragmatik bir başlangıç noktası olmaya devam ediyor ve Bloc, ekibinizin katı mimari korkuluklardan fayda gördüğü durumlarda doğru seçim.
Flutter projeniz için aşamalı geçiş stratejisi veya mimari inceleme isterseniz benimle iletişime geçebilirsiniz.
İlgili Makaleler
Flutter Nedir? Kapsamlı Başlangıç Rehberi
Flutter'ın ne olduğunu, nasıl çalıştığını ve neden modern ürün ekiplerinin tercih ettiğini öğrenin. Dart, widget yapısı ve çoklu platform geliştirme sürecini keşfedin.
Flutter'da Clean Architecture: Ölçeklenebilir Uygulama Geliştirme
Flutter projelerinde Clean Architecture'ı uygulanabilir şekilde öğrenin. Katmanlar, bağımlılık yönetimi ve test edilebilir kod için pratik bir rehber.
Dart Best Practices: Temiz ve Sürdürülebilir Kod Rehberi
Dart projelerinde okunabilir, test edilebilir ve sürdürülebilir kod yazmak için en etkili pratikleri öğrenin.
Flutter Projeniz mi Var?
iOS, Android ve web için yüksek performanslı Flutter uygulamaları geliştiriyorum.
İletişime Geç