Flutter: Riverpod ile Modern State Management
Riverpod, Provider'ın yaratıcısı Remi Rousselet tarafından geliştirilen, Provider'ın tüm sınırlamalarını aşan modern bir state management çözümüdür. Compile-time safety, test edilebilirlik ve esneklik sunar.
State Management Neden Önemli?
Modern uygulamalar karmaşık veri akışlarına sahiptir:
- Kullanıcı girişi ve oturum yönetimi
- Anlık mesajlaşma ve bildirimler
- Çevrimdışı veri senkronizasyonu
- Çoklu API entegrasyonları
- Gerçek zamanlı güncellemeler
Bu karmaşıklığı yönetmek için güçlü bir state management çözümü şarttır.
Provider'ın Sınırlamaları
Provider harika bir araçtır, ancak bazı sınırlamaları vardır:
1. BuildContext Bağımlılığı
// Provider okumak için her zaman context gerekir
final user = Provider.of<UserProvider>(context);
// Context olmayan yerlerde (utility fonksiyonlar, servisler) erişemezsiniz2. Runtime Hatalar
// Provider bulunamazsa, uygulama ÇALIŞIRKEN hata verir
// Compile time'da yakalanamaz
final data = context.read<SomeProvider>(); // ProviderNotFoundException!3. Provider Birleştirme Zorluğu
Bir provider'ın başka bir provider'a bağlı olması durumunda karmaşıklaşır.
4. Hot Reload Sorunları
Provider'lar bazen hot reload'da düzgün güncellenmez.
5. Global State Erişimi
Widget tree dışından state'e erişmek zordur.
Riverpod Bu Sorunları Nasıl Çözer?
✅ BuildContext Gereksiz
// Riverpod: Context olmadan her yerden erişim
final user = ref.watch(userProvider);
// Servis sınıflarında, test'lerde, her yerde çalışır✅ Compile-Time Safety
// Provider bulunamazsa, kod DERLENMEZ
// Hataları çalışmadan önce yakalarsınız
final data = ref.watch(someProvider); // Compile-time kontrol!✅ Kolay Provider Birleştirme
// Bir provider başka provider'ları kolayca izleyebilir
final userOrdersProvider = FutureProvider((ref) async {
final user = await ref.watch(userProvider.future);
return fetchOrders(user.id);
});✅ Mükemmel Test Edilebilirlik
// Provider'ları kolayca override edebilirsiniz
ProviderScope(
overrides: [
userProvider.overrideWithValue(mockUser),
],
child: MyApp(),
)✅ AutoDispose ile Bellek Yönetimi
Kullanılmayan provider'lar otomatik olarak temizlenir.
Ne Zaman Riverpod Kullanmalısınız?
| Durum | Provider | Riverpod |
|---|---|---|
| Basit uygulamalar | ✅ | ✅ |
| Büyük/Kurumsal projeler | ⚠️ | ✅ |
| Çok sayıda bağımlı provider | ❌ | ✅ |
| Yoğun test gereksinimleri | ⚠️ | ✅ |
| Widget tree dışından erişim | ❌ | ✅ |
| Yeni projeler | ⚠️ | ✅ |
Genel Kural: Yeni bir projeye başlıyorsanız veya karmaşık state gereksiniminiz varsa, Riverpod'u tercih edin.
Provider vs Riverpod
| Özellik | Provider | Riverpod |
|---|---|---|
| BuildContext | Gerekli | Gereksiz |
| Compile-time safety | Kısıtlı | Tam |
| Provider birleştirme | Zor | Kolay |
| Test edilebilirlik | Orta | Mükemmel |
| Hot reload | Sorunlu | Sorunsuz |
| Global erişim | BuildContext ile | Her yerden |
⚠️ Önemli Not: Riverpod harici bir pakettir ve DartPad'de çalışmaz. Örnekleri kendi bilgisayarınızda çalıştırmak için aşağıdaki kurulum adımlarını takip edin.
Kurulum
pubspec.yaml dosyanıza ekleyin:
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^2.4.9Ardından terminalde çalıştırın:
flutter pub getTemel Kurulum
Uygulamanızı ProviderScope ile sarmalayın:
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}Provider Türleri
1. Provider (Değişmez Değerler)
// Basit değer
final greetingProvider = Provider<String>((ref) {
return 'Merhaba Flutter!';
});
// Kullanım
class MyWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final greeting = ref.watch(greetingProvider);
return Text(greeting);
}
}2. StateProvider (Basit State)
// Sayaç state'i
final counterProvider = StateProvider<int>((ref) => 0);
// Kullanım
class CounterPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Scaffold(
body: Center(child: Text('$count')),
floatingActionButton: FloatingActionButton(
onPressed: () {
ref.read(counterProvider.notifier).state++;
},
child: Icon(Icons.add),
),
);
}
}3. StateNotifierProvider (Karmaşık State)
// State sınıfı
class CounterNotifier extends StateNotifier<int> {
CounterNotifier() : super(0);
void increment() => state++;
void decrement() => state--;
void reset() => state = 0;
}
// Provider tanımı
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
return CounterNotifier();
});
// Kullanım
class CounterPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Column(
children: [
Text('$count'),
ElevatedButton(
onPressed: () => ref.read(counterProvider.notifier).increment(),
child: Text('Artır'),
),
ElevatedButton(
onPressed: () => ref.read(counterProvider.notifier).decrement(),
child: Text('Azalt'),
),
],
);
}
}4. FutureProvider (Async Veriler)
final userProvider = FutureProvider<User>((ref) async {
final response = await http.get(Uri.parse('https://api.example.com/user'));
return User.fromJson(jsonDecode(response.body));
});
// Kullanım
class UserPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userProvider);
return userAsync.when(
loading: () => CircularProgressIndicator(),
error: (error, stack) => Text('Hata: $error'),
data: (user) => Text('Merhaba ${user.name}'),
);
}
}5. StreamProvider (Stream Veriler)
final messagesProvider = StreamProvider<List<Message>>((ref) {
return FirebaseFirestore.instance
.collection('messages')
.snapshots()
.map((snapshot) => snapshot.docs.map((doc) => Message.fromDoc(doc)).toList());
});
// Kullanım
class MessagesPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final messagesAsync = ref.watch(messagesProvider);
return messagesAsync.when(
loading: () => CircularProgressIndicator(),
error: (error, stack) => Text('Hata: $error'),
data: (messages) => ListView.builder(
itemCount: messages.length,
itemBuilder: (context, index) => ListTile(
title: Text(messages[index].text),
),
),
);
}
}ConsumerWidget vs Consumer
ConsumerWidget (Tüm widget)
class MyPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Text('$count');
}
}Consumer (Sadece bir bölüm)
class MyPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Sayfa')), // Rebuild olmaz
body: Consumer(
builder: (context, ref, child) {
final count = ref.watch(counterProvider);
return Text('$count'); // Sadece bu rebuild olur
},
),
);
}
}ref.watch vs ref.read
// ✅ Build metodunda watch kullan (değişiklikleri dinler)
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Text('$count');
}
// ✅ Event handler'larda read kullan (tek seferlik okuma)
onPressed: () {
ref.read(counterProvider.notifier).increment();
}
// ❌ Build metodunda read kullanma
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.read(counterProvider); // Güncellenme
return Text('$count');
}Gerçek Dünya Örneği: Todo Uygulaması
// Todo modeli
class Todo {
final String id;
final String title;
final bool isCompleted;
Todo({required this.id, required this.title, this.isCompleted = false});
Todo copyWith({String? title, bool? isCompleted}) {
return Todo(
id: id,
title: title ?? this.title,
isCompleted: isCompleted ?? this.isCompleted,
);
}
}
// Todo Notifier
class TodoNotifier extends StateNotifier<List<Todo>> {
TodoNotifier() : super([]);
void addTodo(String title) {
state = [
...state,
Todo(id: DateTime.now().toString(), title: title),
];
}
void toggleTodo(String id) {
state = state.map((todo) {
if (todo.id == id) {
return todo.copyWith(isCompleted: !todo.isCompleted);
}
return todo;
}).toList();
}
void removeTodo(String id) {
state = state.where((todo) => todo.id != id).toList();
}
}
// Provider
final todoProvider = StateNotifierProvider<TodoNotifier, List<Todo>>((ref) {
return TodoNotifier();
});
// Filtre provider'ı
enum TodoFilter { all, completed, uncompleted }
final todoFilterProvider = StateProvider<TodoFilter>((ref) => TodoFilter.all);
// Filtrelenmiş todo listesi
final filteredTodosProvider = Provider<List<Todo>>((ref) {
final todos = ref.watch(todoProvider);
final filter = ref.watch(todoFilterProvider);
switch (filter) {
case TodoFilter.completed:
return todos.where((todo) => todo.isCompleted).toList();
case TodoFilter.uncompleted:
return todos.where((todo) => !todo.isCompleted).toList();
case TodoFilter.all:
return todos;
}
});
// UI
class TodoPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final todos = ref.watch(filteredTodosProvider);
return Scaffold(
appBar: AppBar(
title: Text('Görevler'),
actions: [
PopupMenuButton<TodoFilter>(
onSelected: (filter) {
ref.read(todoFilterProvider.notifier).state = filter;
},
itemBuilder: (context) => [
PopupMenuItem(value: TodoFilter.all, child: Text('Tümü')),
PopupMenuItem(value: TodoFilter.completed, child: Text('Tamamlanan')),
PopupMenuItem(value: TodoFilter.uncompleted, child: Text('Tamamlanmayan')),
],
),
],
),
body: ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
final todo = todos[index];
return ListTile(
leading: Checkbox(
value: todo.isCompleted,
onChanged: (_) {
ref.read(todoProvider.notifier).toggleTodo(todo.id);
},
),
title: Text(
todo.title,
style: TextStyle(
decoration: todo.isCompleted
? TextDecoration.lineThrough
: null,
),
),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () {
ref.read(todoProvider.notifier).removeTodo(todo.id);
},
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddDialog(context, ref),
child: Icon(Icons.add),
),
);
}
void _showAddDialog(BuildContext context, WidgetRef ref) {
final controller = TextEditingController();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Yeni Görev'),
content: TextField(
controller: controller,
decoration: InputDecoration(hintText: 'Görev adı'),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('İptal'),
),
ElevatedButton(
onPressed: () {
if (controller.text.isNotEmpty) {
ref.read(todoProvider.notifier).addTodo(controller.text);
Navigator.pop(context);
}
},
child: Text('Ekle'),
),
],
),
);
}
}Provider Birleştirme
// Kullanıcı provider'ı
final userProvider = FutureProvider<User>((ref) async {
return await fetchUser();
});
// Kullanıcının siparişleri (userProvider'a bağlı)
final userOrdersProvider = FutureProvider<List<Order>>((ref) async {
final user = await ref.watch(userProvider.future);
return await fetchOrders(user.id);
});
// Toplam sipariş tutarı
final totalOrderAmountProvider = Provider<double>((ref) {
final ordersAsync = ref.watch(userOrdersProvider);
return ordersAsync.when(
loading: () => 0,
error: (_, __) => 0,
data: (orders) => orders.fold(0, (sum, order) => sum + order.amount),
);
});Family Modifier
Parametreli provider oluşturma:
// Ürün detayı provider'ı
final productProvider = FutureProvider.family<Product, String>((ref, productId) async {
final response = await http.get(
Uri.parse('https://api.example.com/products/$productId'),
);
return Product.fromJson(jsonDecode(response.body));
});
// Kullanım
class ProductPage extends ConsumerWidget {
final String productId;
ProductPage({required this.productId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final productAsync = ref.watch(productProvider(productId));
return productAsync.when(
loading: () => CircularProgressIndicator(),
error: (error, stack) => Text('Hata: $error'),
data: (product) => Text(product.name),
);
}
}AutoDispose Modifier
Provider otomatik temizleme:
// Widget dispose olduğunda provider da temizlenir
final searchProvider = FutureProvider.autoDispose<List<Product>>((ref) async {
// Debounce için
await Future.delayed(Duration(milliseconds: 500));
// İptal kontrolü
if (ref.state.isRefreshing) return [];
return await searchProducts();
});ref.listen ile Yan Etkiler
class MyPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// State değiştiğinde yan etki çalıştır
ref.listen<int>(counterProvider, (previous, next) {
if (next == 10) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('10\'a ulaştınız!')),
);
}
});
return Text('${ref.watch(counterProvider)}');
}
}ref.invalidate ile Yenileme
// Provider'ı yeniden çalıştır
ElevatedButton(
onPressed: () {
ref.invalidate(userProvider);
},
child: Text('Yenile'),
)Best Practices
1. Provider'ları global tanımlayın
// ✅ Dosyanın en üstünde
final counterProvider = StateProvider<int>((ref) => 0);
// ❌ Widget içinde tanımlamayın
class MyWidget extends ConsumerWidget {
final counterProvider = StateProvider<int>((ref) => 0); // Yanlış!
}2. Küçük, odaklı provider'lar oluşturun
// ✅ Doğru - Her provider tek bir iş yapar
final userProvider = FutureProvider<User>(...);
final userOrdersProvider = FutureProvider<List<Order>>(...);
final userBalanceProvider = Provider<double>(...);
// ❌ Yanlış - Çok fazla iş yapan provider
final everythingProvider = FutureProvider<Everything>(...);3. select ile gereksiz rebuild'leri önleyin
// ✅ Sadece name değişince rebuild olur
final userName = ref.watch(userProvider.select((user) => user.name));
// ❌ User'ın herhangi bir alanı değişince rebuild olur
final user = ref.watch(userProvider);Özet
- Riverpod: Provider'ın gelişmiş versiyonu
- ProviderScope: Uygulamayı sarmalayan kapsam
- StateProvider: Basit state için
- StateNotifierProvider: Karmaşık state için
- FutureProvider: Async veriler için
- StreamProvider: Stream veriler için
- ref.watch: Build'de kullan (dinler)
- ref.read: Event'lerde kullan (dinlemez)
- family: Parametreli provider
- autoDispose: Otomatik temizleme
Riverpod, büyük ve karmaşık Flutter projelerinde state yönetimi için mükemmel bir çözümdür.