İleri Seviye Riverpod: Families, Notifiers ve Test Stratejileri
# Flutter Uygulamalarinda Ileri Seviye Riverpod Kaliplari
Riverpod, Flutter projelerimde varsayilan state management cozumum haline geldi. Ancak "Riverpod'a giris" ile "Riverpod'u uretim ortaminda dogru kullanmak" arasinda buyuk bir ucurum var. Bu yazida, birden fazla yayinlanmis uygulamada rafine ettigim kaliplari, tuzaklari ve mimari kararlari paylasiyorum.
Provider Family ve Parametreli Provider'lar
Provider family'ler, parametre alan provider'lar olusturmanizi saglar. Ayni veri cekme mantigi farkli girdilere uygulandiginda -- ornegin kullanici profilini ID'ye gore yuklemek veya urunleri kategoriye gore getirmek gibi -- bu yapi vazgecilmezdir.
Temel Family Kullanimi
final userProvider = FutureProvider.autoDispose.family<User, String>(
(ref, userId) async {
final api = ref.watch(apiClientProvider);
return api.getUser(userId);
},
);
class=class="code-string">"code-comment">// Widget icerisinde
class UserProfileScreen extends ConsumerWidget {
final String userId;
const UserProfileScreen({super.key, required this.userId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userProvider(userId));
return userAsync.when(
loading: () => const LoadingIndicator(),
error: (err, stack) => ErrorDisplay(message: err.toString()),
data: (user) => UserProfileView(user: user),
);
}
}Karmasik Parametreler icin Record Kullanimi
Bir provider birden fazla parametre gerektirdiginde, Dart record'lari ozel bir parametre sinifi olusturmadan zarif bir cozum sunar.
typedef ProductFilter = ({String category, int page, SortOrder sort});
final productsProvider = FutureProvider.autoDispose
.family<List<Product>, ProductFilter>((ref, filter) async {
final api = ref.watch(apiClientProvider);
return api.getProducts(
category: filter.category,
page: filter.page,
sort: filter.sort,
);
});
class=class="code-string">"code-comment">// Kullanim
final products = ref.watch(productsProvider((
category: class="code-string">'electronics',
page: class="code-number">1,
sort: SortOrder.priceAsc,
)));Uretim uygulamalarimda record'larin mukemmel bir denge kurdigunu gordum: tam bir sinifin yuku olmadan isimlendirilmis alanlar sunuyorlar ve Riverpod esitlik karsilastirmasini kutudan ciktiginda dogru sekilde hallediyor.
StateNotifier ve AsyncNotifier Kaliplari
StateNotifier Ne Zaman Hala Mantikli?
Riverpod 2.0'dan bu yana `Notifier` ve `AsyncNotifier` onerilen yaklasim olsa da, mevcut kod tabanlarinda `StateNotifier` ile karsilasacaksiniz. Her ikisini de anlamak onemlidir.
class=class="code-string">"code-comment">// Eski StateNotifier yaklasimi
class CartNotifier extends StateNotifier<List<CartItem>> {
final ApiClient _api;
CartNotifier(this._api) : super([]);
Future<void> addItem(Product product) async {
state = [...state, CartItem.fromProduct(product)];
await _api.syncCart(state);
}
void removeItem(String itemId) {
state = state.where((item) => item.id != itemId).toList();
}
}
final cartProvider =
StateNotifierProvider<CartNotifier, List<CartItem>>((ref) {
return CartNotifier(ref.watch(apiClientProvider));
});Modern AsyncNotifier Kaliplari
Asenkron is akislari icin Riverpod'un gercekten parlayan noktasi `AsyncNotifier`'dir. `build` metodu ilk asenkron yuklemeyi tanimlar, sonraki mutasyonlar ise hata yonetimini koruyarak state'i guncelleyebilir.
class ProductListNotifier
extends AsyncNotifier<List<Product>> {
@override
Future<List<Product>> build() async {
final api = ref.watch(apiClientProvider);
return api.fetchProducts();
}
Future<void> refresh() async {
state = const AsyncLoading();
state = await AsyncValue.guard(() => build());
}
Future<void> deleteProduct(String id) async {
final previousState = state;
class=class="code-string">"code-comment">// Iyimser guncelleme
state = AsyncData(
state.value!.where((p) => p.id != id).toList(),
);
try {
await ref.read(apiClientProvider).deleteProduct(id);
} catch (e, stack) {
class=class="code-string">"code-comment">// Basarisizlikta geri al
state = previousState;
state = AsyncError(e, stack);
}
}
}
final productListProvider =
AsyncNotifierProvider<ProductListNotifier, List<Product>>(
ProductListNotifier.new,
);Uretim uygulamalarimda yukaridaki iyimser guncelleme kalibi, duyarli arayuzler olusturmak icin paha bicilemez oldu. Kullanicilar ag istegi arka planda gerceklesirken aninda geri bildirim goruyor ve bir seyler ters giderse state temiz bir sekilde geri aliniyor.
Family + AsyncNotifier Kombinasyonu
class UserOrdersNotifier
extends FamilyAsyncNotifier<List<Order>, String> {
@override
Future<List<Order>> build(String userId) async {
final api = ref.watch(apiClientProvider);
return api.getUserOrders(userId);
}
Future<void> cancelOrder(String orderId) async {
await ref.read(apiClientProvider).cancelOrder(orderId);
ref.invalidateSelf();
await future;
}
}
final userOrdersProvider = AsyncNotifierProvider.family<
UserOrdersNotifier, List<Order>, String>(
UserOrdersNotifier.new,
);Hangi Provider Tipini Ne Zaman Kullanmali?
Dogru provider tipini secmek, gelistiricilerin en cok takildigi ilk kararlardan biridir. Iste kullandigim karar tablosu.
| Senaryo | Provider Tipi | Neden |
|---|---|---|
| Basit turetilmis deger | `Provider` | Hesaplanmis state, yan etki yok |
| Asenkron veri cekme (salt okunur) | `FutureProvider` | Otomatik yukleme/hata/veri durumlari |
| Gercek zamanli akis | `StreamProvider` | WebSocket, Firestore snapshot'lari |
| Degistirilebilir senkron state | `NotifierProvider` | Form state'i, toggle'lar, sayaclar |
| Degistirilebilir asenkron state | `AsyncNotifierProvider` | Yukleme durumlariyla CRUD islemleri |
| Parametreli salt okunur | `FutureProvider.family` | Ayni sorgu, farkli girdiler |
| Parametreli degistirilebilir | `AsyncNotifierProvider.family` | Varlık bazinda islemler |
| Tek seferlik kurulum | `Provider` | Servisler icin bagimlilk enjeksiyonu |
Kararsiz kaldiginizda, okumalar icin `FutureProvider`, yazmalar icin `AsyncNotifierProvider` ile baslayin. Daha sonra her zaman yeniden duzenleyebilirsiniz.
riverpod_generator ile Kod Uretimi
Kod uretimi, boilerplate'i azaltir ve yapilandirma hatalarini derleme zamaninda yakalar. Uretim uygulamalarimda kod uretimini bastan benimsemenin sonradan ciddi yeniden duzenleme tasarrufu sagladigini gordum.
Kurulum
# pubspec.yaml
dependencies:
flutter_riverpod: ^2.5.0
riverpod_annotation: ^2.3.0
dev_dependencies:
riverpod_generator: ^2.4.0
build_runner: ^2.4.0Pratikte Uretilmis Provider'lar
import class="code-string">'package:riverpod_annotation/riverpod_annotation.dart';
part class="code-string">'auth_provider.g.dart';
class=class="code-string">"code-comment">// Basit provider — Provider<ApiClient>class="code-string">'in yerini alir
@riverpod
ApiClient apiClient(ApiClientRef ref) {
return ApiClient(baseUrl: 'https:class=class="code-string">"code-comment">//api.example.comclass="code-string">');
}
class=class="code-string">"code-comment">// Family ile FutureProvider — FutureProvider.family'nin yerini alir
@riverpod
Future<User> userProfile(UserProfileRef ref, String userId) async {
final api = ref.watch(apiClientProvider);
return api.getUser(userId);
}
class=class="code-string">"code-comment">// AsyncNotifier — AsyncNotifierProvider'in yerini alir
@riverpod
class TodoList extends _$TodoList {
@override
Future<List<Todo>> build() async {
final api = ref.watch(apiClientProvider);
return api.fetchTodos();
}
Future<void> addTodo(String title) async {
final api = ref.read(apiClientProvider);
final newTodo = await api.createTodo(title);
state = AsyncData([...state.value ?? [], newTodo]);
}
}KeepAlive ve AutoDispose Kontrolu
class=class="code-string">"code-comment">// Bu provider tum uygulama omru boyunca hayatta kalir
@Riverpod(keepAlive: true)
class AuthState extends _$AuthState {
@override
Future<AuthUser?> build() async {
return ref.watch(authRepositoryProvider).getCurrentUser();
}
}
class=class="code-string">"code-comment">// Bu provider artik izlenmediginde otomatik temizlenir (varsayilan)
@riverpod
Future<List<Product>> searchProducts(
SearchProductsRef ref,
String query,
) async {
class=class="code-string">"code-comment">// Aramayi geciktir
await Future.delayed(const Duration(milliseconds: class="code-number">300));
if (ref.state is AsyncLoading) return [];
final api = ref.watch(apiClientProvider);
return api.searchProducts(query);
}Provider'lari Test Etme
Test yazmak, Riverpod'un mimarisinin gercekten karsiligini verdigi noktadir. Widget agaclarina, BuildContext'e veya karmasik mock framework'lerine ihtiyaciniz yoktur. Tek gereken bir `ProviderContainer`'dir.
Notifier Birim Testi
void main() {
late ProviderContainer container;
late MockApiClient mockApi;
setUp(() {
mockApi = MockApiClient();
container = ProviderContainer(
overrides: [
apiClientProvider.overrideWithValue(mockApi),
],
);
addTearDown(container.dispose);
});
test(class="code-string">'fetchProducts urun listesi dondurur', () async {
final expectedProducts = [
Product(id: class="code-string">'class="code-number">1', name: class="code-string">'Widget A'),
Product(id: class="code-string">'class="code-number">2', name: class="code-string">'Widget B'),
];
when(() => mockApi.fetchProducts())
.thenAnswer((_) async => expectedProducts);
final subscription = container.listen(
productListProvider,
(_, __) {},
);
await container.read(productListProvider.future);
expect(
container.read(productListProvider).value,
equals(expectedProducts),
);
subscription.close();
});
test(class="code-string">'deleteProduct iyimser guncelleme yapar', () async {
when(() => mockApi.fetchProducts())
.thenAnswer((_) async => [Product(id: class="code-string">'class="code-number">1', name: class="code-string">'Test')]);
when(() => mockApi.deleteProduct(class="code-string">'class="code-number">1'))
.thenAnswer((_) async {});
container.listen(productListProvider, (_, __) {});
await container.read(productListProvider.future);
await container
.read(productListProvider.notifier)
.deleteProduct(class="code-string">'class="code-number">1');
expect(
container.read(productListProvider).value,
isEmpty,
);
});
}Birden Fazla Override ile Test
ProviderContainer createTestContainer({
AuthUser? currentUser,
List<Product>? products,
}) {
return ProviderContainer(
overrides: [
authStateProvider.overrideWith(() => MockAuthNotifier(currentUser)),
productListProvider.overrideWith(() => MockProductNotifier(products)),
analyticsProvider.overrideWithValue(MockAnalytics()),
],
);
}
test(class="code-string">'admin kullanici urunleri silebilir', () async {
final container = createTestContainer(
currentUser: AuthUser(role: UserRole.admin),
products: [Product(id: class="code-string">'class="code-number">1', name: class="code-string">'Test')],
);
addTearDown(container.dispose);
final canDelete = container.read(canDeleteProductProvider);
expect(canDelete, isTrue);
});Widget Testlerinde ProviderScope Override'lari
testWidgets(class="code-string">'ProductListScreen yukleme sonrasi verileri gosterir', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
productListProvider.overrideWith(
() => FakeProductNotifier([
Product(id: class="code-string">'class="code-number">1', name: class="code-string">'Test Urun'),
]),
),
],
child: const MaterialApp(home: ProductListScreen()),
),
);
class=class="code-string">"code-comment">// Basta yukleme gosterir
expect(find.byType(CircularProgressIndicator), findsOneWidget);
class=class="code-string">"code-comment">// Asenkron islem tamamlandiktan sonra
await tester.pumpAndSettle();
expect(find.text(class="code-string">'Test Urun'), findsOneWidget);
});Riverpod + Clean Architecture Entegrasyonu
Clean Architecture ve Riverpod, net sinirlar olusturdugunda birbirini iyi tamamlar. Iste uretim projelerimde kullandigim yapi.
Katman Ayirimi
lib/
features/
products/
data/
datasources/
product_remote_datasource.dart
product_local_datasource.dart
models/
product_model.dart
repositories/
product_repository_impl.dart
domain/
entities/
product.dart
repositories/
product_repository.dart # soyut
usecases/
get_products.dart
delete_product.dart
presentation/
providers/
product_providers.dart
screens/
product_list_screen.dart
widgets/
product_card.dartProvider'lar ile Baglanti Kurma
class=class="code-string">"code-comment">// Veri katmani — somut uygulamalar
@riverpod
ProductRemoteDataSource productRemoteDataSource(
ProductRemoteDataSourceRef ref,
) {
final dio = ref.watch(dioProvider);
return ProductRemoteDataSourceImpl(dio);
}
class=class="code-string">"code-comment">// Domain katmani — soyut repository, veri katmani uzerinden saglaniyor
@riverpod
ProductRepository productRepository(ProductRepositoryRef ref) {
return ProductRepositoryImpl(
remote: ref.watch(productRemoteDataSourceProvider),
local: ref.watch(productLocalDataSourceProvider),
network: ref.watch(networkInfoProvider),
);
}
class=class="code-string">"code-comment">// Use caseclass="code-string">'ler
@riverpod
GetProducts getProducts(GetProductsRef ref) {
return GetProducts(ref.watch(productRepositoryProvider));
}
class=class="code-string">"code-comment">// Sunum katmani — use case'leri UI state'ine baglar
@riverpod
class ProductListController extends _$ProductListController {
@override
Future<List<Product>> build() async {
final getProducts = ref.watch(getProductsProvider);
final result = await getProducts(NoParams());
return result.fold(
(failure) => throw failure,
(products) => products,
);
}
}Temel kavrayis sudur: provider'lar bagimlilk enjeksiyon konteyneri gorevi gorur. Her katman yalnizca altindaki katmanin soyutlamalarina bagimlidir ve Riverpod somut uygulamalari bir araya getirir.
Yaygin Riverpod Anti-Pattern'leri
1. build metodu icerisinde ref.read kullanmak
class=class="code-string">"code-comment">// YANLIS: ref.read deger degistiginde yeniden cizimi tetiklemez
@override
Widget build(BuildContext context, WidgetRef ref) {
final user = ref.read(userProvider); class=class="code-string">"code-comment">// Hata: guncellenmez
return Text(user.name);
}
class=class="code-string">"code-comment">// DOGRU: ref.watch deger degistiginde widget'i yeniden cizer
@override
Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(userProvider);
return Text(user.name);
}2. Callback'ler icerisinde ref.watch kullanmak
class=class="code-string">"code-comment">// YANLIS: callback icerisinde ref.watch ongongorulemeyen davranislara neden olur
onPressed: () {
final api = ref.watch(apiProvider); class=class="code-string">"code-comment">// Callbackclass="code-string">'te watch kullanmayin
api.submit();
}
class=class="code-string">"code-comment">// DOGRU: callback'lerde tek seferlik islemler icin ref.read kullanin
onPressed: () {
final api = ref.read(apiProvider);
api.submit();
}3. Her seyi yapan sismis provider'lar
class=class="code-string">"code-comment">// YANLIS: auth, profil, ayarlar ve bildirimleri tek providerclass="code-string">'da yonetmek
@riverpod
class AppState extends _$AppState {
@override
Future<AppData> build() async {
final user = await fetchUser();
final settings = await fetchSettings();
final notifications = await fetchNotifications();
return AppData(user, settings, notifications);
}
}
class=class="code-string">"code-comment">// DOGRU: sorumlulukları odakli provider'lara ayirmak
@riverpod
Future<User> currentUser(CurrentUserRef ref) async { ... }
@riverpod
Future<Settings> userSettings(UserSettingsRef ref) async { ... }
@riverpod
Future<List<Notification>> notifications(NotificationsRef ref) async { ... }4. Navigasyona bagli veriler icin autoDispose'u ihmal etmek
class=class="code-string">"code-comment">// YANLIS: kullanici sayfadan ayrildiginda bu provider bellek sizintisina neden olur
final searchResultsProvider = FutureProvider<List<Product>>((ref) async {
return ref.watch(apiProvider).search(ref.watch(searchQueryProvider));
});
class=class="code-string">"code-comment">// DOGRU: autoDispose ekran artik gorunmediginde temizlik yapar
final searchResultsProvider =
FutureProvider.autoDispose<List<Product>>((ref) async {
return ref.watch(apiProvider).search(ref.watch(searchQueryProvider));
});5. Dongusel provider bagimliliklari
class=class="code-string">"code-comment">// YANLIS: providerA providerBclass="code-string">'yi izler, providerB de providerA'yi izler
class=class="code-string">"code-comment">// Bu calisma zamaninda ProviderExceptionclass="code-string">'a neden olur
class=class="code-string">"code-comment">// DOGRU: bagimliliklarin tek yonde akmasini saglayacak sekilde yeniden yapilandirin
class=class="code-string">"code-comment">// Paylasilmis mantigi her ikisinin de bagimli olabilecegi ucuncu bir provider'a cikarinUretim uygulamalarimda en cok gercek dunya acisindan 3 numarali anti-pattern'in sorun yarattgini gordum. Tek bir devasa provider birden fazla sorumlulufu ustlendiginde, bildirimlerdeki bir degisiklik kullanici profilinin gereksiz yere yeniden cekilmesine neden oluyor. Provider'lari kucuk ve odakli tutmak, Riverpod'un bagimlilik grafiginin isini verimli yapmasini saglar.
Performans Ipuclari
select ile Secici Yeniden Cizimler
class=class="code-string">"code-comment">// Herhangi bir kullanici degisikliginde yeniden cizmek yerine
final user = ref.watch(userProvider);
class=class="code-string">"code-comment">// Sadece belirli alan degistiginde yeniden ciz
final userName = ref.watch(
userProvider.select((user) => user.name),
);Provider Yeniden Cizimlerini Geciktirme
@riverpod
Future<List<Product>> searchProducts(
SearchProductsRef ref,
String query,
) async {
class=class="code-string">"code-comment">// 300ms icerisinde yeni sorgu gelirse iptal et
ref.keepAlive();
final link = ref.keepAlive();
final timer = Timer(const Duration(seconds: class="code-number">30), link.close);
ref.onDispose(timer.cancel);
class=class="code-string">"code-comment">// Geciktirme
await Future.delayed(const Duration(milliseconds: class="code-number">300));
final api = ref.watch(apiClientProvider);
return api.search(query);
}Sonuc
Riverpod, bilingli mimari kararlari odullendirir. Bu yazidaki kaliplar -- parametreli family'ler, iyimser guncellemelerle AsyncNotifier, kod uretimi, clean architecture sinirlariyla odakli provider'lar -- kucuk bir ozellikten karmasik bir uretim uygulamasina olceklenen bir arac seti olusturur. Yalnizca test hikayesi bile -- `ProviderContainer` ve override'larin yuzlerce satirlik mock kurulumunun yerini almasi -- Riverpod'u yatirima deger kilar.
Dogru provider tipini secmek icin karar tablosuyla baslayin, kod uretimini erken benimseyin, provider'larinizi kucuk ve odakli tutun; Flutter uygulamalariniz bakimi keyifli hale gelecektir.
Flutter projeniz icin Riverpod mimarisi tasarlamak veya mevcut kod tabaninizi modern Riverpod kaliplarina tasimak icin benimle iletisime gecebilirsiniz.
İlgili Makaleler
Flutter State Management: Riverpod, Provider ve Bloc Karşılaştırması
Flutter'da state management yaklaşımlarını karşılaştırın. Riverpod, Provider ve Bloc için kullanım senaryolarını ve karar kriterlerini netleştirin.
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.
Flutter Testing Rehberi: Unit, Widget ve Integration Test
Flutter projelerinde test stratejisi kurun. Unit, widget ve integration test katmanlarını doğru sorumluluklarla yapılandırın.
Flutter Projeniz mi Var?
iOS, Android ve web için yüksek performanslı Flutter uygulamaları geliştiriyorum.
İletişime Geç