İçeriğe geç / Skip to content / Zum Inhalt

Flutter: Riverpod 3 ile Modern State Management

Ahmet Balaman

Son güncelleme:

8 dk okuma

FlutterRiverpodState ManagementNotifierAsyncNotifierProvider
Flutter: Riverpod 3 ile Modern State Management

Riverpod, Provider paketinin yazarı Remi Rousselet tarafından geliştirilen bir state management çözümü. Provider'ın InheritedWidget üzerine kurulu olmasından kaynaklanan sınırlamaları gidermek için tasarlandı: eksik provider hatalarını derleme anına taşıyor, provider'ları birleştirmeyi kolaylaştırıyor ve test etmeyi sadeleştiriyor. Kurulum adımlarının ve "Hello world" örneklerinin tamamı Riverpod'un resmî başlangıç dokümanında yer alıyor.

Bu yazı Riverpod 3 (flutter_riverpod 3.x) ile yazıldı. İnternetteki eski örneklerin çoğu Riverpod 2 ile yazılmış ve StateProvider ya da StateNotifier kullanıyor. Bunlar Riverpod 3'te hâlâ var ama "legacy" (eski) olarak ayrı bir dosyaya taşındı; yazının sonunda eski kodu nasıl okuyup güncelleyeceğinizi ayrıca anlatıyorum.

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

Aynı veriyi birden fazla ekranın okuduğu, bir verinin başka bir veriye bağlı olduğu bu tür durumlarda state'i düzenli bir yapıda tutmak gerekiyor.

Provider'ın Sınırlamaları

Provider iyi bir araç; ChangeNotifier, Consumer ve MultiProvider ile nasıl çalıştığını Provider yazısında anlattım. Riverpod'un dokümantasyonu, Provider'ın şu sınırlamalarını Riverpod'un çıkış sebebi olarak sayıyor:

1. Okuma için BuildContext gerekir

// Provider, InheritedWidget üzerine kurulu: okumak için context gerekir
final user = Provider.of<UserProvider>(context);

Bir provider'ın create fonksiyonu içinde başka bir provider'ı context.watch ile okumak da güvenilir değildir.

2. Eksik provider hatası çalışma anında çıkar

// Provider ağaçta yoksa uygulama ÇALIŞIRKEN hata verir
final data = context.read<SomeProvider>(); // ProviderNotFoundException

Bu hata özellikle yeniden düzenleme (refactor) sırasında sık görülür.

3. Provider birleştirmek zahmetlidir

Bir provider başka bir provider'a bağlı olduğunda ProxyProvider gibi yapılar gerekir ve kod hızla karmaşıklaşır.

4. Aynı tipte iki provider güvenilir çalışmaz

İki ayrı Provider<Item> tanımlarsanız InheritedWidget bunlardan yalnızca birini bulur.

5. Otomatik temizleme ve parametre desteği yok

Provider, bir değeri artık kimsenin dinlemediğini bilemez; bu yüzden kullanılmayan state'i kendiliğinden temizleyemez. Parametreye göre farklı değer üreten (örneğin ürün ID'sine göre ürün) bir provider tanımlamanın da hazır bir yolu yoktur.

Riverpod Bu Sorunları Nasıl Çözer?

✅ Provider'lar widget ağacına bağlı değil, ama bir ref gerekiyor

Riverpod'da provider'lar global değişken olarak tanımlanır; state ise uygulamanın en üstündeki ProviderScope içinde tutulur. Bir provider'ı okumak için BuildContext yerine bir ref kullanırsınız. Bu, "context olmadan her yerden erişim" anlamına gelmiyor; ref'i nereden aldığınız bağlama göre değişiyor:

// 1) Widget içinde: ConsumerWidget'ın verdiği WidgetRef
final user = ref.watch(userProvider);

// 2) Başka bir provider içinde: provider'ın kendi Ref'i
final greetingProvider = Provider<String>((ref) {
  final name = ref.watch(userNameProvider);
  return 'Merhaba $name';
});

// 3) Widget ağacı dışında (test, Dart betiği): ProviderContainer
final container = ProviderContainer();
final name = container.read(userNameProvider);
container.dispose();

Sıradan bir servis sınıfının içinden provider okuyamazsınız; o sınıfın ya kendisi bir provider olmalı ya da ihtiyacı olan değeri dışarıdan almalı. Asıl kazanç, provider'ların birbirini ref.watch ile okuyabilmesi ve bunun için widget ağacına hiç ihtiyaç duymaması.

✅ Eksik provider derleme anında yakalanır

// Provider bir değişken: yanlış yazılmış ya da tanımlanmamışsa kod DERLENMEZ
final data = ref.watch(someProvider);

Riverpod'da ProviderNotFoundException diye bir şey yok. Tek şart, uygulamanın kökünü ProviderScope ile sarmak; bunu unutursanız ilk okumada çalışma anında hata alırsınız.

✅ Kolay provider birleştirme

// Bir provider başka provider'ları izleyebilir
final userOrdersProvider = FutureProvider<List<Order>>((ref) async {
  final user = await ref.watch(userProvider.future);
  return fetchOrders(user.id);
});

userProvider değiştiğinde userOrdersProvider kendiliğinden yeniden hesaplanır.

✅ Test edilebilirlik

// Testte gerçek API yerine sahte veri
final container = ProviderContainer.test(
  overrides: [
    userProvider.overrideWith((ref) async => fakeUser),
  ],
);

ProviderContainer.test() Riverpod 3 ile geldi ve test bitince kendini temizliyor. Widget testlerinde aynı overrides listesini ProviderScope'a verebilirsiniz.

✅ autoDispose ile bellek yönetimi

.autoDispose ile tanımlanan bir provider, onu dinleyen kalmadığında state'ini temizler.

Riverpod 3'te Neler Değişti?

Riverpod 2'den gelen biri için en önemli değişiklikler şunlar (resmî "What's new" sayfası):

  • Notifier ve AsyncNotifier tek önerilen yol. StateProvider, StateNotifierProvider ve ChangeNotifierProvider artık package:flutter_riverpod/legacy.dart içinden içe aktarılıyor ve yeni kodda önerilmiyor.
  • AutoDispose ve Family sınıfları birleşti. AutoDisposeNotifier, FamilyNotifier gibi ayrı sınıflar yok; family parametresi Notifier'a constructor üzerinden geliyor.
  • Ref artık generic değil ve async işlemlerden sonra provider'ın hâlâ yaşayıp yaşamadığını kontrol etmek için ref.mounted var.
  • Otomatik yeniden deneme: Hata veren provider'lar varsayılan olarak artan aralıklarla yeniden deneniyor. İstemiyorsanız ProviderScope(retry: (retryCount, error) => null, ...) ile kapatabilirsiniz.
  • Görünmeyen widget'ların dinlemesi duraklatılıyor: Ekranda olmayan widget'lar provider'ları dinlemeyi geçici olarak bırakıyor.
  • Tüm provider'lar değişikliği == ile filtreliyor. Yeni değer eskisine eşitse dinleyiciler bilgilendirilmiyor.
  • AsyncValue sealed oldu ve valueOrNull yerine value kullanılıyor.

Ne Zaman Riverpod Kullanmalısınız?

Durum Provider Riverpod
Basit uygulamalar ✅ ✅
Büyük projeler ⚠️ ✅
Çok sayıda bağımlı provider ❌ ✅
Yoğun test gereksinimleri ⚠️ ✅
Widget ağacı dışından okuma ❌ ✅ (ProviderContainer ile)
Yeni projeler ⚠️ ✅

Genel kural: Yeni bir projeye başlıyorsanız veya birbirine bağlı çok sayıda state'iniz varsa Riverpod iyi bir tercih.

Provider vs Riverpod

Özellik Provider Riverpod
Okuma BuildContext ile ref ile (WidgetRef, Ref, ProviderContainer)
Eksik provider Çalışma anında hata Derleme hatası
Aynı tipte iki provider Güvenilir değil Sorunsuz
Provider birleştirme ProxyProvider ile, zahmetli ref.watch ile
Otomatik temizleme Yok autoDispose
Parametreli provider Yok family

Her ikisinden de önce, tek bir ekrana ait geçici veriyi widget'ın kendi state'inde tutmanın yeterli olduğunu unutmayın; initState ve dispose'un bu işi nasıl üstlendiğini yaşam döngüsü yazısında bulabilirsiniz.

⚠️ Ö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

En kolayı paketi terminalden eklemek; bu komut pub.dev'deki en güncel sürümü pubspec.yaml'a yazar:

flutter pub add flutter_riverpod

Elle eklemek isterseniz güncel sürümü flutter_riverpod'un pub.dev sayfasından kontrol edin. Bu yazı güncellendiğinde son sürüm 3.4.3'tü:

dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: ^3.4.3

Kod üretimiyle (@riverpod anotasyonu) çalışmak isterseniz riverpod_annotation ve riverpod_generator paketleri de var. Bu yazıdaki örnekler kod üretimi olmadan, doğrudan yazılabilen sözdizimini kullanıyor.

Temel Kurulum

Uygulamanızı ProviderScope ile sarmalayın:

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() {
  runApp(
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

Provider Türleri

1. Provider (Değişmeyen ya da türetilen değerler)

// Basit değer
final greetingProvider = Provider<String>((ref) {
  return 'Merhaba Flutter!';
});

// Kullanım
class MyWidget extends ConsumerWidget {
  const MyWidget({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final greeting = ref.watch(greetingProvider);
    return Text(greeting);
  }
}

2. NotifierProvider (Değişen state)

Riverpod 3'te değişen state için önerilen yol Notifier. Başlangıç değerini build() metodu döndürür, state'i değiştiren işlemler sınıfın metotlarıdır:

class Counter extends Notifier<int> {
  @override
  int build() => 0; // Başlangıç değeri

  void increment() => state++;
  void decrement() => state--;
  void reset() => state = 0;
}

final counterProvider = NotifierProvider<Counter, int>(Counter.new);

// Kullanım
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('$count')),
      floatingActionButton: FloatingActionButton(
        onPressed: () => ref.read(counterProvider.notifier).increment(),
        child: const Icon(Icons.add),
      ),
    );
  }
}

ref.watch(counterProvider) değeri (int), ref.read(counterProvider.notifier) ise Counter nesnesini verir. State'i sınıfın dışından ref.read(counterProvider.notifier).state = 5 şeklinde değiştirmeyin: state setter'ı @protected işaretli ve analiz aracı uyarı verir. Değişikliği her zaman bir metot üzerinden yapın; böylece state'i kimin nasıl değiştirdiği tek bir sınıfta toplanır.

3. FutureProvider (Salt okunur async veri)

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 {
  const UserPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final userAsync = ref.watch(userProvider);

    return userAsync.when(
      loading: () => const CircularProgressIndicator(),
      error: (error, stack) => Text('Hata: $error'),
      data: (user) => Text('Merhaba ${user.name}'),
    );
  }
}

AsyncValue Riverpod 3'te sealed bir sınıf olduğu için when yerine Dart'ın switch ifadesini de kullanabilirsiniz:

return switch (userAsync) {
  AsyncData(:final value) => Text('Merhaba ${value.name}'),
  AsyncError(:final error) => Text('Hata: $error'),
  _ => const CircularProgressIndicator(),
};

4. AsyncNotifierProvider (Async veri + değiştirme)

Veriyi sunucudan yükleyip sonra üzerinde işlem de yapacaksanız AsyncNotifier kullanın. build() burada bir Future döndürür:

class ProfileNotifier extends AsyncNotifier<User> {
  @override
  Future<User> build() => fetchUser();

  Future<void> rename(String newName) async {
    final current = await future;
    state = const AsyncLoading();
    state = await AsyncValue.guard(
      () => saveUser(current.copyWith(name: newName)),
    );
  }
}

final profileProvider =
    AsyncNotifierProvider<ProfileNotifier, User>(ProfileNotifier.new);

// Kullanım: ref.watch(profileProvider) bir AsyncValue<User> döndürür
// ref.read(profileProvider.notifier).rename('Ayşe');

AsyncValue.guard, işlem hata fırlatırsa state'i AsyncError'a, başarılı olursa AsyncData'ya çevirir; try/catch yazmanıza gerek kalmaz.

5. StreamProvider (Akan 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 {
  const MessagesPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final messagesAsync = ref.watch(messagesProvider);

    return messagesAsync.when(
      loading: () => const 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 {
  const MyPage({super.key});

  @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 {
  const MyPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Sayfa')), // Rebuild olmaz
      body: Consumer(
        builder: (context, ref, child) {
          final count = ref.watch(counterProvider);
          return Text('$count'); // Sadece bu rebuild olur
        },
      ),
    );
  }
}

initState, dispose ya da bir TextEditingController gerektiren ekranlarda ConsumerStatefulWidget ve ConsumerState kullanılır; ref orada State sınıfının bir alanı olarak hazır gelir.

ref.watch vs ref.read

ref.watch build içinde değeri okur ve değişince yeniden çizer, ref.read olay içinde değeri bir kez okur ve dinlemez

// ✅ 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); // Değer değişince güncellenmez
  return Text('$count');
}

Gerçek Dünya Örneği: Todo Uygulaması

// Todo modeli
class Todo {
  final String id;
  final String title;
  final bool isCompleted;

  const 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,
    );
  }
}

// Görev listesi
class TodoList extends Notifier<List<Todo>> {
  @override
  List<Todo> build() => [];

  void add(String title) {
    state = [
      ...state,
      Todo(id: DateTime.now().microsecondsSinceEpoch.toString(), title: title),
    ];
  }

  void toggle(String id) {
    state = [
      for (final todo in state)
        if (todo.id == id) todo.copyWith(isCompleted: !todo.isCompleted) else todo,
    ];
  }

  void remove(String id) {
    state = state.where((todo) => todo.id != id).toList();
  }
}

final todoListProvider = NotifierProvider<TodoList, List<Todo>>(TodoList.new);

// Filtre
enum TodoFilter { all, completed, uncompleted }

class TodoFilterNotifier extends Notifier<TodoFilter> {
  @override
  TodoFilter build() => TodoFilter.all;

  void change(TodoFilter filter) => state = filter;
}

final todoFilterProvider =
    NotifierProvider<TodoFilterNotifier, TodoFilter>(TodoFilterNotifier.new);

// Filtrelenmiş liste: iki provider'dan türetilir
final filteredTodosProvider = Provider<List<Todo>>((ref) {
  final todos = ref.watch(todoListProvider);
  final filter = ref.watch(todoFilterProvider);

  return switch (filter) {
    TodoFilter.completed => todos.where((todo) => todo.isCompleted).toList(),
    TodoFilter.uncompleted => todos.where((todo) => !todo.isCompleted).toList(),
    TodoFilter.all => todos,
  };
});

// UI
class TodoPage extends ConsumerWidget {
  const TodoPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final todos = ref.watch(filteredTodosProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Görevler'),
        actions: [
          PopupMenuButton<TodoFilter>(
            onSelected: (filter) {
              ref.read(todoFilterProvider.notifier).change(filter);
            },
            itemBuilder: (context) => const [
              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(todoListProvider.notifier).toggle(todo.id),
            ),
            title: Text(
              todo.title,
              style: TextStyle(
                decoration: todo.isCompleted ? TextDecoration.lineThrough : null,
              ),
            ),
            trailing: IconButton(
              icon: const Icon(Icons.delete),
              onPressed: () => ref.read(todoListProvider.notifier).remove(todo.id),
            ),
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _showAddDialog(context, ref),
        child: const Icon(Icons.add),
      ),
    );
  }

  void _showAddDialog(BuildContext context, WidgetRef ref) {
    final controller = TextEditingController();

    showDialog<void>(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Yeni Görev'),
        content: TextField(
          controller: controller,
          decoration: const InputDecoration(hintText: 'Görev adı'),
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('İptal'),
          ),
          ElevatedButton(
            onPressed: () {
              if (controller.text.isNotEmpty) {
                ref.read(todoListProvider.notifier).add(controller.text);
                Navigator.pop(context);
              }
            },
            child: const Text('Ekle'),
          ),
        ],
      ),
    );
  }
}

Riverpod 2 ile yazılmış örneklerde filtre için genellikle StateProvider görürsünüz. Riverpod 3'te aynı işi yukarıdaki gibi küçük bir Notifier yapıyor; değişiklik change metodu üzerinden geçtiği için kodu okuyan biri filtrenin nereden değiştiğini tek bakışta görebiliyor.

Provider Birleştirme

// Kullanıcı provider'ı
final userProvider = FutureProvider<User>((ref) async {
  return 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 fetchOrders(user.id);
});

// Toplam sipariş tutarı
final totalOrderAmountProvider = Provider<double>((ref) {
  final orders = ref.watch(userOrdersProvider).value ?? [];
  return orders.fold(0.0, (sum, order) => sum + order.amount);
});

Riverpod 3'te AsyncValue.value, veri yoksa (yükleniyor ya da hata durumunda) null döndürür; Riverpod 2'deki valueOrNull'ın yerini aldı.

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;

  const ProductPage({super.key, required this.productId});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final productAsync = ref.watch(productProvider(productId));

    return productAsync.when(
      loading: () => const CircularProgressIndicator(),
      error: (error, stack) => Text('Hata: $error'),
      data: (product) => Text(product.name),
    );
  }
}

Notifier ile family kullanırken parametre Riverpod 3'te constructor'dan gelir:

class ProductQuantity extends Notifier<int> {
  ProductQuantity(this.productId);
  final String productId;

  @override
  int build() => 1;

  void increment() => state++;
}

final productQuantityProvider =
    NotifierProvider.family<ProductQuantity, int, String>(ProductQuantity.new);

// Kullanım
// ref.watch(productQuantityProvider('urun-42'));
// ref.read(productQuantityProvider('urun-42').notifier).increment();

Family parametreleri == ile karşılaştırılır. Her build'de yeni oluşturulan bir liste ya da =='i tanımlanmamış bir nesne verirseniz her seferinde yeni bir provider oluşur; parametre olarak String, int ya da =='i doğru tanımlanmış değer nesneleri kullanın.

AutoDispose Modifier

Kimse dinlemediğinde state'i temizlemek için .autoDispose eklenir. Arama kutusu gibi her tuşta yeniden çalışan bir istek için Riverpod dokümantasyonundaki "debounce" (bekletme) kalıbı şöyle:

final searchProvider =
    FutureProvider.autoDispose.family<List<Product>, String>((ref, query) async {
  // Kullanıcı yazmaya devam ederse bu provider dispose edilir
  var didDispose = false;
  ref.onDispose(() => didDispose = true);

  await Future<void>.delayed(const Duration(milliseconds: 500));
  if (didDispose) {
    throw Exception('İptal edildi');
  }

  return searchProducts(query);
});

Riverpod 3'te aynı kontrolü if (!ref.mounted) ... ile de yapabilirsiniz; ref.mounted, await sonrasında provider'ın hâlâ yaşayıp yaşamadığını söyler.

ref.listen ile Yan Etkiler

class MyPage extends ConsumerWidget {
  const MyPage({super.key});

  @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(
          const 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: const Text('Yenile'),
)

En İyi Uygulamalar

1. Provider'ları global tanımlayın

// ✅ Dosyanın en üst seviyesinde
final counterProvider = NotifierProvider<Counter, int>(Counter.new);

// ❌ Widget içinde tanımlamayın: her build'de yeni bir provider oluşur
class MyWidget extends ConsumerWidget {
  final counterProvider = NotifierProvider<Counter, int>(Counter.new); // 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 görev sayısı değişince rebuild olur
final count = ref.watch(todoListProvider.select((todos) => todos.length));

// ❌ Listedeki herhangi bir değişiklikte rebuild olur
final todos = ref.watch(todoListProvider);

Eski Kod: StateProvider ve StateNotifier

Riverpod 2 ile yazılmış bir projeyi 3'e yükselttiğinizde StateProvider ve StateNotifierProvider kullanan dosyalar derlenmez; çünkü bu sınıflar ana içe aktarmadan çıkarıldı. İki seçeneğiniz var.

Hızlı çözüm: Eski sınıfları kullanan dosyalara legacy içe aktarmasını ekleyin. Kod olduğu gibi çalışmaya devam eder:

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod/legacy.dart'; // StateProvider, StateNotifierProvider, ChangeNotifierProvider

Kalıcı çözüm: Zamanla Notifier'a geçin. Dönüşüm çoğunlukla mekanik:

// Riverpod 2: StateNotifier
class CounterNotifier extends StateNotifier<int> {
  CounterNotifier() : super(0);
  void increment() => state++;
}
final counterProvider =
    StateNotifierProvider<CounterNotifier, int>((ref) => CounterNotifier());

// Riverpod 3: Notifier
class Counter extends Notifier<int> {
  @override
  int build() => 0; // super(0) yerine build()
  void increment() => state++;
}
final counterProvider = NotifierProvider<Counter, int>(Counter.new);

Widget tarafındaki ref.watch(counterProvider) ve ref.read(counterProvider.notifier).increment() çağrıları değişmiyor. StateProvider kullanan yerlerde ise ref.read(provider.notifier).state = x yerine, yukarıdaki filtre örneğindeki gibi küçük bir Notifier ve bir metot yazıyorsunuz. Adım adım rehber için Riverpod 3 geçiş dokümanına bakabilirsiniz.

Özet

  • Riverpod: Provider'ın sınırlamalarını gidermek için tasarlanmış state management paketi
  • ProviderScope: Uygulamayı sarmalayan, state'in tutulduğu kapsam
  • ref: Provider okumak için gereken nesne (WidgetRef, Ref, ProviderContainer)
  • NotifierProvider: Değişen senkron state için
  • AsyncNotifierProvider: Yüklenen ve değiştirilen async state için
  • FutureProvider / StreamProvider: Salt okunur async veriler için
  • ref.watch: Build'de kullan (dinler)
  • ref.read: Event'lerde kullan (dinlemez)
  • family: Parametreli provider
  • autoDispose: Otomatik temizleme
  • legacy.dart: Eski StateProvider ve StateNotifierProvider kodu için

Riverpod, birbirine bağlı çok sayıda state'i olan Flutter projelerinde state yönetimi için güçlü bir çözüm. Tek bir asenkron isteği ekrana basmak gibi dar bir ihtiyaç için pakete hiç gerek olmayabilir; o durumda FutureBuilder yeterli olur.

Sık Sorulan Sorular

Provider'dan Riverpod'a geçmeli miyim?

Çalışan bir Provider kurulumunu sırf yenilik için değiştirmeye gerek yok. Yeni bir projeye başlıyorsanız veya ProviderNotFoundException ve provider birleştirme zorlukları canınızı sıkıyorsa Riverpod'a geçmek mantıklı; geçiş kademeli yapılabilir.

Riverpod 2 ile yazdığım kod Riverpod 3'te çalışır mı?

Büyük kısmı çalışır. StateProvider, StateNotifierProvider ve ChangeNotifierProvider kullanan dosyalara package:flutter_riverpod/legacy.dart içe aktarmasını eklemeniz gerekir. AutoDisposeNotifier ve FamilyNotifier gibi sınıflar kaldırıldığı için onları Notifier'a çevirmeniz, family parametresini constructor'a taşımanız gerekir.

ref.watch ile ref.read arasındaki fark nedir?

ref.watch değeri okur ve provider değiştiğinde widget'ı yeniden build eder; build metodunun içinde kullanılır. ref.read yalnızca o anki değeri alır, aboneliğe yol açmaz ve buton geri çağrıları gibi olay işleyicilerinde tercih edilir.

ConsumerWidget ile Consumer'ı ne zaman kullanmalıyım?

Widget'ın tamamı provider'a bağlıysa ConsumerWidget daha sade bir yazım sunar. Ekranın yalnızca küçük bir bölümü değişiyorsa o bölümü Consumer ile sarın; böylece yeniden build edilen ağaç küçük kalır.

Yorumlar