İleri Seviye Riverpod: Families, Notifiers ve Test Stratejileri

12 dakika okuma9 Mart 2026
Riverpod advancedRiverpod familiesAsyncNotifier FlutterRiverpod testingriverpod_generatorFlutter state management advancedRiverpod best practicesRiverpod code generation

# 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

dart
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.

dart
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.

dart
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.

dart
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

dart
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

yaml
# pubspec.yaml
dependencies:
  flutter_riverpod: ^2.5.0
  riverpod_annotation: ^2.3.0

dev_dependencies:
  riverpod_generator: ^2.4.0
  build_runner: ^2.4.0

Pratikte Uretilmis Provider'lar

dart
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

dart
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

dart
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

dart
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

dart
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.dart

Provider'lar ile Baglanti Kurma

dart
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

dart
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

dart
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

dart
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

dart
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

dart
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 cikarin

Uretim 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

dart
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

dart
@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 Projeniz mi Var?

iOS, Android ve web için yüksek performanslı Flutter uygulamaları geliştiriyorum.

İletişime Geç