Flutter: Provider ile State Management
Son güncelleme:
4 dk okuma

Provider, Flutter'da state management için en popüler paketlerden biridir; Flutter ekibinin basit state yönetimi rehberi de örneklerini bu paketle anlatır. Widget tree boyunca veriyi paylaşmak ve değişiklikleri dinlemek için kullanılır.
State Nedir ve Neden Yönetilmeli?
State, uygulamanızın herhangi bir anda sahip olduğu veri durumudur. Örneğin:
- Kullanıcının giriş yapıp yapmadığı
- Sepetteki ürün sayısı
- Tema tercihi (açık/koyu)
- Form alanlarındaki değerler
- API'den gelen veriler
State yönetimi olmadan, bu verileri widget'lar arasında paylaşmak ve güncellemeleri yansıtmak çok zorlaşır.
setState'in Sınırlamaları
Flutter'da basit state yönetimi için setState kullanılır; setState'in StatefulWidget içindeki yerini ve ne zaman çalıştığını yaşam döngüsü yazısında anlatmıştım. Ancak uygulama büyüdükçe ciddi sorunlarla karşılaşırsınız:
1. Prop Drilling Problemi
// Veriyi 5 seviye aşağıya geçirmek zorunda kalırsınız
GrandParent(
child: Parent(
child: Child(
child: GrandChild(
child: GreatGrandChild(
userData: userData, // Tüm yol boyunca geçirilmeli!
),
),
),
),
)2. Gereksiz Rebuild'ler
setState kullandığınızda, tüm widget yeniden build olur - sadece değişen kısım değil.
3. State Kaybı
Widget tree'de gezinirken state kaybolabilir. Sayfa değiştirip geri döndüğünüzde veriler sıfırlanır.
4. Test Edilemezlik
UI ve business logic iç içe geçtiğinde birim testleri yazmak zorlaşır.
5. Kod Tekrarı
Aynı veriyi birden fazla yerde kullanmak için sürekli parametre geçirmek gerekir.
Provider Bu Sorunları Nasıl Çözer?
✅ Merkezi State Yönetimi
Veriler tek bir yerden yönetilir, her yerden erişilebilir.
✅ Verimli Rebuild'ler
Sadece değişen widget'lar yeniden build edilir.
✅ Sorumlulukların Ayrılması (Separation of Concerns)
Business logic ve UI birbirinden ayrılır.
✅ Kolay Test Edilebilirlik
Provider'ları mock'layarak kolayca test yazabilirsiniz.
✅ Prop Drilling Yok
Veriye ihtiyaç duyan widget, doğrudan Provider'dan okur.
Ne Zaman Provider Kullanmalısınız?
| Durum | setState | Provider |
|---|---|---|
| Tek widget'ta basit sayaç | ✅ | ❌ |
| Form validasyonu | ✅ | ❌ |
| Birden fazla sayfada kullanıcı bilgisi | ❌ | ✅ |
| Sepet yönetimi | ❌ | ✅ |
| Tema/Dil ayarları | ❌ | ✅ |
| API'den gelen veriler | ❌ | ✅ |
| Karmaşık form state'leri | ❌ | ✅ |
Genel Kural: Eğer state birden fazla widget veya sayfa tarafından kullanılıyorsa, Provider düşünün.
Neden Provider?
| Özellik | setState | Provider |
|---|---|---|
| Kapsam | Tek widget | Tüm widget tree |
| Karmaşıklık | Basit | Orta |
| Ölçeklenebilirlik | Zor | Kolay |
| Test edilebilirlik | Zor | Kolay |
| Prop drilling | Gerekli | Gereksiz |
Provider'ın BuildContext bağımlılığından ve derleme zamanı güvenliğinden rahatsızsanız, aynı fikrin bir sonraki adımı Riverpod: provider'ları global tanımlar, context'e ihtiyaç duymaz ve yanlış tipte okumayı derleme zamanında yakalar.
⚠️ Önemli Not: Provider 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
Güncel sürümü provider paketinin pub.dev sayfasından görebilirsiniz; pubspec.yaml dosyanıza ekleyin:
dependencies:
flutter:
sdk: flutter
provider: ^6.1.1Ardından terminalde çalıştırın:
flutter pub getTemel Kavramlar
1. ChangeNotifier
State'i tutan ve değişiklikleri bildiren sınıf; Flutter'ın kendi parçası olan ChangeNotifier sınıfının API dokümanı dinleyici ekleme ve notifyListeners davranışını ayrıntısıyla anlatır:
import 'package:flutter/foundation.dart';
class CounterProvider extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners(); // Dinleyicilere haber ver
}
void decrement() {
_count--;
notifyListeners();
}
void reset() {
_count = 0;
notifyListeners();
}
}2. ChangeNotifierProvider
Provider'ı widget tree'ye ekleme:
import 'package:provider/provider.dart';
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CounterProvider(),
child: MyApp(),
),
);
}3. Consumer
State'i dinleme ve UI güncelleme:
Consumer<CounterProvider>(
builder: (context, counter, child) {
return Text(
'${counter.count}',
style: TextStyle(fontSize: 48),
);
},
)4. context.read ve context.watch
// Okuma (değişiklikleri dinlemez) - Butonlarda kullan
context.read<CounterProvider>().increment();
// İzleme (değişiklikleri dinler) - Build metodunda kullan
final count = context.watch<CounterProvider>().count;Tam Örnek: Sayaç Uygulaması
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// 1. State sınıfı
class CounterProvider extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
void decrement() {
_count--;
notifyListeners();
}
}
// 2. Main
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CounterProvider(),
child: MaterialApp(
home: CounterPage(),
),
),
);
}
// 3. UI
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Provider Sayaç')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Sayaç Değeri:'),
Consumer<CounterProvider>(
builder: (context, counter, child) {
return Text(
'${counter.count}',
style: TextStyle(fontSize: 72, fontWeight: FontWeight.bold),
);
},
),
SizedBox(height: 32),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FloatingActionButton(
onPressed: () => context.read<CounterProvider>().decrement(),
child: Icon(Icons.remove),
),
SizedBox(width: 16),
FloatingActionButton(
onPressed: () => context.read<CounterProvider>().increment(),
child: Icon(Icons.add),
),
],
),
],
),
),
);
}
}MultiProvider
Birden fazla provider kullanma:
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => CounterProvider()),
ChangeNotifierProvider(create: (_) => ThemeProvider()),
ChangeNotifierProvider(create: (_) => UserProvider()),
],
child: MyApp(),
),
);
}Gerçek Dünya Örneği: Tema Değiştirme
// Theme Provider
class ThemeProvider extends ChangeNotifier {
ThemeMode _themeMode = ThemeMode.light;
ThemeMode get themeMode => _themeMode;
bool get isDarkMode => _themeMode == ThemeMode.dark;
void toggleTheme() {
_themeMode = isDarkMode ? ThemeMode.light : ThemeMode.dark;
notifyListeners();
}
}
// Main
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => ThemeProvider(),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer<ThemeProvider>(
builder: (context, themeProvider, child) {
return MaterialApp(
themeMode: themeProvider.themeMode,
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
home: HomePage(),
);
},
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final themeProvider = context.watch<ThemeProvider>();
return Scaffold(
appBar: AppBar(title: Text('Tema Ayarları')),
body: Center(
child: SwitchListTile(
title: Text('Karanlık Mod'),
value: themeProvider.isDarkMode,
onChanged: (_) => themeProvider.toggleTheme(),
),
),
);
}
}Bu hâliyle tercih yalnızca bellekte tutulur ve uygulama kapanınca açık temaya döner. Seçimi kalıcı yapmak için aynı ChangeNotifier'ın değeri diske nasıl yazıp açılışta nasıl okuyacağını SharedPreferences ile yerel veri saklama yazısında adım adım anlattım.
Gerçek Dünya Örneği: Alışveriş Sepeti
// Ürün modeli
class Product {
final String id;
final String name;
final double price;
Product({required this.id, required this.name, required this.price});
}
// Sepet Provider
class CartProvider extends ChangeNotifier {
final List<Product> _items = [];
List<Product> get items => List.unmodifiable(_items);
int get itemCount => _items.length;
double get totalPrice => _items.fold(0, (sum, item) => sum + item.price);
void addItem(Product product) {
_items.add(product);
notifyListeners();
}
void removeItem(String productId) {
_items.removeWhere((item) => item.id == productId);
notifyListeners();
}
void clearCart() {
_items.clear();
notifyListeners();
}
}
// Kullanım
class ProductCard extends StatelessWidget {
final Product product;
ProductCard({required this.product});
@override
Widget build(BuildContext context) {
return Card(
child: ListTile(
title: Text(product.name),
subtitle: Text('₺${product.price}'),
trailing: IconButton(
icon: Icon(Icons.add_shopping_cart),
onPressed: () {
context.read<CartProvider>().addItem(product);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${product.name} sepete eklendi')),
);
},
),
),
);
}
}
// Sepet ikonu (AppBar'da)
class CartIcon extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Stack(
children: [
IconButton(
icon: Icon(Icons.shopping_cart),
onPressed: () {
// Sepet sayfasına git
},
),
Positioned(
right: 0,
top: 0,
child: Consumer<CartProvider>(
builder: (context, cart, child) {
return cart.itemCount > 0
? CircleAvatar(
radius: 10,
backgroundColor: Colors.red,
child: Text(
'${cart.itemCount}',
style: TextStyle(fontSize: 12, color: Colors.white),
),
)
: SizedBox.shrink();
},
),
),
],
);
}
}Selector ile Optimizasyon
Sadece belirli değişiklikleri dinleme:
// Tüm değişiklikleri dinler (verimsiz)
Consumer<CartProvider>(
builder: (context, cart, child) {
return Text('${cart.itemCount} ürün');
},
)
// Sadece itemCount değişince rebuild (verimli)
Selector<CartProvider, int>(
selector: (context, cart) => cart.itemCount,
builder: (context, itemCount, child) {
return Text('$itemCount ürün');
},
)Provider Türleri
| Tür | Kullanım |
|---|---|
Provider |
Değişmeyen değerler |
ChangeNotifierProvider |
Değişen state (en yaygın) |
FutureProvider |
Async veriler |
StreamProvider |
Stream veriler |
ProxyProvider |
Bağımlı provider'lar |
FutureProvider Örneği
FutureProvider<List<Product>>(
create: (_) => fetchProducts(),
initialData: [],
child: ProductList(),
)En İyi Uygulamalar
1. Provider'ı mümkün olduğunca yukarıda tanımlayın
// ✅ Doğru - Main'de
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => MyProvider(),
child: MyApp(),
),
);
}
// ❌ Yanlış - Derinlerde
class SomePage extends StatelessWidget {
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => MyProvider(), // Her build'de yeni instance!
child: ...,
);
}
}2. context.read vs context.watch
// ✅ Build metodunda watch kullan
Widget build(BuildContext context) {
final count = context.watch<CounterProvider>().count;
return Text('$count');
}
// ✅ Event handler'larda read kullan
onPressed: () {
context.read<CounterProvider>().increment();
}
// ❌ Build metodunda read kullanma
Widget build(BuildContext context) {
final count = context.read<CounterProvider>().count; // Güncellenme
return Text('$count');
}3. Consumer'ı mümkün olduğunca dar tutun
// ✅ Doğru - Sadece gerekli kısım rebuild olur
Scaffold(
appBar: AppBar(title: Text('Sayfa')),
body: Consumer<CounterProvider>(
builder: (context, counter, child) {
return Text('${counter.count}');
},
),
)
// ❌ Yanlış - Tüm sayfa rebuild olur
Consumer<CounterProvider>(
builder: (context, counter, child) {
return Scaffold(
appBar: AppBar(title: Text('Sayfa')),
body: Text('${counter.count}'),
);
},
)Özet
Provider: Flutter için önerilen state management çözümüChangeNotifier: State tutan ve bildiren sınıfnotifyListeners(): UI'ı güncellemek için çağrılırConsumer: State değişikliklerini dinlercontext.watch: Build'de kullan (dinler)context.read: Event'lerde kullan (dinlemez)MultiProvider: Birden fazla providerSelector: Performans optimizasyonu
Provider, küçükten büyüğe tüm Flutter projelerinde kullanılabilecek güçlü ve esnek bir state management çözümüdür. Tek bir ekranın kendi verisi söz konusuysa setState hâlâ en basit yol; asenkron tek seferlik veriyi ekrana basmak içinse FutureBuilder çoğu zaman yeterlidir.
Sık Sorulan Sorular
Provider mı setState mi kullanmalıyım?
State yalnızca tek bir widget'ın içinde yaşıyor ve dışarıdan kimse okumuyorsa setState en basit ve en hızlı yoldur. Aynı veriyi birden fazla ekran okuyor veya güncelliyorsa Provider'a geçin; asıl kazanç prop drilling'den kurtulmaktır.
context.read ile context.watch arasındaki fark nedir?
context.watch değeri okur ve o widget'ı değişikliklere abone eder, bu yüzden yalnızca build içinde kullanılır. context.read abone olmadan tek seferlik okuma yapar ve buton onPressed gibi olay işleyicilerinde tercih edilir.
notifyListeners() çağırdım ama ekran güncellenmiyor, neden?
En sık iki sebep var: dinleyen widget context.read kullanıyordur (abone olmaz) ya da listeyi yerinde değiştirip aynı referansı döndürüyorsunuzdur. Değişikliği ChangeNotifier içinde yapıp notifyListeners() çağırdığınızdan ve okuyan tarafın context.watch veya Consumer kullandığından emin olun.
İlgili Yazılar
Flutter: Riverpod 3 ile Modern State Management
Riverpod 3 ile Flutter'da state yönetimi: Notifier, AsyncNotifier, ref.watch ve ref.read, family, autoDispose ve eski StateNotifier kodu için geçiş notları.
Flutter Yaşam Döngüsü (Life Cycle): StatefulWidget Rehberi
Flutter'da StatefulWidget yaşam döngüsü metodları ve uygulama yaşam döngüsü yönetimi. initState, dispose, didUpdateWidget ve setState kullanımı.
Flutter'da Navigator ile Sayfalar Arası Geçiş ve Veri Transferi
Flutter'da Navigator ile sayfa geçişi, veri aktarımı ve back stack yönetimi: push, pop, pushReplacement ve geri tuşu için güncel PopScope kullanımı.