Ana içeriğe atla

Motivasyon

Bu derinlemesine yazının amacı, Riverpod'un neden var olduğunu göstermektir.

Özellikle bu bölüm şu sorulara yanıt vermelidir:

  • Provider bu kadar yaygınken, neden Riverpod'a geçilsin?
  • Somut olarak ne gibi avantajlar elde ediyorum?
  • Riverpod'a nasıl geçebilirim?
  • Kademeli olarak geçiş yapabilir miyim?
  • vb.

Bu bölümün sonunda, Riverpod'un Provider'a tercih edilmesi gerektiğine ikna olmuş olmalısınız.

Riverpod, Provider ile karşılaştırıldığında gerçekten de daha modern, önerilen ve güvenilir bir yaklaşımdır.

Riverpod daha iyi durum yönetimi yetenekleri, daha iyi önbellekleme stratejileri ve sadeleştirilmiş bir tepkisellik modeli sunar. Provider ise şu anda pek çok alanda yetersiz kalıyor ve ilerlemek için bir yol da yok.

Provider'ın kısıtlamaları

Provider, InheritedWidget API'si tarafından sınırlandırıldığı için temel sorunlara sahiptir.
Doğası gereği Provider "daha basit bir InheritedWidget"tir; Provider yalnızca bir InheritedWidget sarmalayıcısıdır ve dolayısıyla onunla sınırlıdır.

İşte bilinen Provider sorunlarının bir listesi.

Provider aynı "tip"ten iki (veya daha fazla) provider'ı bir arada tutamaz

İki Provider<Item> tanımlamak güvenilmez bir davranışa yol açar: InheritedWidget'ın API'si ikisinden yalnızca birini elde eder: en yakın Provider<Item> atasını.
Provider'ın dokümantasyonunda bir geçici çözüm anlatılsa da, Riverpod'da bu sorun hiç yoktur.

Bu kısıtlamayı ortadan kaldırarak mantığı şu şekilde küçük parçalara özgürce bölebiliriz:

final itemsProvider = Provider.autoDispose(
(ref) => <Item>[], // ...
);

final evenItemsProvider = Provider.autoDispose((ref) {
final items = ref.watch(itemsProvider);
return [...items.whereIndexed((index, element) => index.isEven)];
});

Provider'lar makul biçimde tek seferde yalnızca bir değer yayar

Harici bir RESTful API okunurken, yeni bir çağrı bir sonraki değeri yüklerken son okunan değeri göstermek oldukça yaygındır.
Riverpod bu davranışa, AsyncValue API'leri sayesinde tek seferde iki değer yayarak (yani önceki veri değeri ve gelen yeni yükleniyor değeri) izin verir:

final itemsApiProvider = FutureProvider.autoDispose((ref) async {
final client = Dio();
final result = await client.get<List<dynamic>>('your-favorite-api');
final parsed = [...result.data!.map((e) => Item.fromJson(e as Json))];
return parsed;
});

final evenItemsProvider = Provider.autoDispose((ref) {
final asyncValue = ref.watch(itemsApiProvider);
if (asyncValue.isLoading) return <Item>[];
if (asyncValue.hasError) return const [Item(id: -1)];

final items = asyncValue.requireValue;

return [...items.whereIndexed((index, element) => index.isEven)];
});

Önceki kod parçasında evenItemsProvider'ı dinlemek şu etkileri doğuracaktır:

  1. Başlangıçta istek yapılır. Boş bir liste elde ederiz;
  2. Ardından, diyelim ki bir hata oluştu. [Item(id: -1)] elde ederiz;
  3. Sonra, aşağı çekerek yenileme mantığıyla isteği yeniden deneriz (örneğin ref.invalidate ile);
  4. İlk provider'ı yeniden yüklerken, ikincisi hâlâ [Item(id: -1)] sunmaya devam eder;
  5. Bu kez ayrıştırılmış veri doğru şekilde alınır: çift sayılı öğelerimiz doğru biçimde döndürülür.

Provider ile yukarıdaki özelliklere yaklaşmak bile mümkün değildir ve geçici çözüm bulmak daha da zordur.

Provider'ları birleştirmek zor ve hataya açıktır

Provider ile, provider'ın create metodunun içinde context.watch kullanmak cazip gelebilir.
Bu güvenilmez olurdu; çünkü hiçbir bağımlılık değişmemiş olsa bile didChangeDependencies tetiklenebilir (örneğin widget ağacında bir GlobalKey söz konusu olduğunda).

Yine de Provider'ın ProxyProvider adında özel bir çözümü var, ancak bu yorucu ve hataya açık kabul edilir.

Durumu birleştirmek Riverpod'un temel mekanizmalarından biridir; ref.watch ve ref.listen gibi basit ama güçlü araçlarla değerleri sıfır ek yükle tepkisel biçimde birleştirip önbelleğe alabiliriz:

final numberProvider = Provider.autoDispose((ref) {
return Random().nextInt(10);
});

final doubledProvider = Provider.autoDispose((ref) {
final number = ref.watch(numberProvider);

return number * 2;
});

Riverpod ile değerleri birleştirmek doğal hissettirir: bağımlılıklar okunabilirdir ve API'ler aynı kalır.

Güvenlik eksikliği

Provider ile, yeniden düzenlemeler ve/veya büyük değişiklikler sırasında bir ProviderNotFoundException ile karşılaşmak yaygındır.
Gerçekten de bu çalışma zamanı istisnası, Riverpod'un en başta oluşturulmasının başlıca nedenlerinden biri idi.

Bundan çok daha fazla fayda sağlamasının yanı sıra, Riverpod bu istisnayı basitçe fırlatamaz.

Durumu yok etmek zordur

InheritedWidget bir tüketici onu dinlemeyi bıraktığında tepki veremez.
Bu, Provider'ın artık kullanılmayan provider'larının durumunu otomatik olarak yok etme yeteneğine sahip olmasını engeller.
Provider ile, durum kullanılmayı bıraktığında yok etmek için provider'ları kapsamlamaya (scoping) güvenmek zorundayız.
Ancak bu kolay değildir; çünkü durum sayfalar arasında paylaşıldığında işler çetrefilleşir.

Riverpod bunu autodispose ve [Ref.keepAlive] gibi anlaşılması kolay API'lerle çözer.
Bu iki API, esnek ve yaratıcı önbellekleme stratejilerine (örneğin zamana dayalı önbellekleme) olanak tanır:

final diceRollProvider = Provider.autoDispose((ref) {
// Bu provider .autoDispose olduğu için, onu dinlemeyi bırakmak
// o anda sunduğu durumu yok edecektir.
// Ardından bu provider yeniden dinlendiğinde,
// yeni bir zar atılıp tekrar sunulacaktır.
final dice = Random().nextInt(10);
return dice.isEven;
});

final cachedDiceRollProvider = Provider.autoDispose((ref) {
final coin = Random().nextInt(10);
if (coin > 5) throw Exception('Way too large.');
// Yukarıdaki koşul başarısız olabilir;
// Olmazsa, aşağıdaki komut Provider'a artık kimse onu dinlemese bile
// önbelleğe alınmış durumunu korumasını söyler.
ref.keepAlive();
return coin.isEven;
});

Ne yazık ki bunu ham bir InheritedWidget ile, dolayısıyla Provider ile uygulamanın hiçbir yolu yoktur.

Güvenilir bir parametreleme mekanizmasının olmayışı

Riverpod, kullanıcısının .family değiştiricisi ile "parametreli" Provider'lar tanımlamasına izin verir.
Gerçekten de .family, Riverpod'un en güçlü özelliklerinden biridir ve yeniliklerinin merkezinde yer alır; örneğin mantığın muazzam ölçüde sadeleşmesini sağlar.

Benzer bir şeyi Provider kullanarak uygulamak isteseydik, bu parametreler üzerinde hem kullanım kolaylığından hem de tip güvenliğinden vazgeçmemiz gerekirdi.

Dahası, Provider ile benzer bir .autoDispose mekanizması uygulanamaması, doğası gereği .familynin eşdeğer bir uygulamasını da engeller; çünkü bu iki özellik el ele gider.

Son olarak, daha önce gösterildiği gibi, widget'ların bir InheritedWidget'ı dinlemeyi asla bırakmadığı ortaya çıkıyor.
Bu, bir provider durumu "dinamik olarak" bağlanıyorsa, yani bir Provider'ı oluşturmak için parametreler kullanılıyorsa -ki .family tam olarak bunu yapar- ciddi bellek sızıntıları anlamına gelir.
Dolayısıyla Provider için .family eşdeğeri elde etmek şu an itibarıyla temelden imkânsızdır.

Test etmek yorucudur

Bir test yazabilmek için provider'ları her testin içinde yeniden tanımlamak zorundasınız.

Riverpod ile provider'lar varsayılan olarak testlerin içinde kullanıma hazırdır. Ayrıca Riverpod, Provider'ların sahtesini oluştururken kritik öneme sahip kullanışlı bir "geçersiz kılma" araç koleksiyonu sunar.

Yukarıdaki birleştirilmiş durum kod parçasını test etmek şu kadar basit olurdu:

void main() {
test('it doubles the value correctly', () {
final container = ProviderContainer(
overrides: [numberProvider.overrideWith((ref) => 9)],
);
final doubled = container.read(doubledProvider);
expect(doubled, 9 * 2);
});
}

Yan etkileri tetiklemek doğrudan değildir

InheritedWidget'ın bir onChange geri çağırması olmadığı için Provider'ın da olamaz.
Bu, snackbar'lar, modal'lar vb. gibi gezinme senaryoları için sorun yaratır.

Bunun yerine Riverpod, Flutter ile iyi bütünleşen ref.listen'ı sunar.

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


Widget build(BuildContext context, WidgetRef ref) {
ref.listen(diceRollProvider, (previous, next) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Dice roll! We got: $next')),
);
});
return TextButton.icon(
onPressed: () => ref.invalidate(diceRollProvider),
icon: const Icon(Icons.casino),
label: const Text('Roll a dice'),
);
}
}

Riverpod'a doğru

Kavramsal olarak Riverpod ve Provider oldukça benzerdir. Her iki paket de benzer bir rolü doldurur. Her ikisi de şunları yapmaya çalışır:

  • durum tutan bazı nesneleri önbelleğe almak ve yok etmek;
  • testler sırasında bu nesnelerin sahtesini oluşturmanın bir yolunu sunmak;
  • Widget'ların bu nesneleri basit bir şekilde dinlemesi için bir yol sunmak.

Riverpod'u, Provider birkaç yıl daha olgunlaşmaya devam etseydi olabileceği şey olarak düşünebilirsiniz.

Neden ayrı bir paket?

Başlangıçta, yukarıda bahsedilen sorunları çözmenin bir yolu olarak Provider'ın büyük bir sürümünün yayınlanması planlanıyordu.
Ancak daha sonra bundan vazgeçildi; çünkü yeni ConsumerWidget API'si nedeniyle bu "fazla kırıcı" ve hatta tartışmalı olurdu.
Provider hâlâ en çok kullanılan Flutter paketlerinden biri olduğu için, bunun yerine ayrı bir paket oluşturulmasına karar verildi ve böylece Riverpod doğdu.

Ayrı bir paket oluşturmak şunları mümkün kıldı:

  • İsteyen herkes için geçiş kolaylığı; her iki yaklaşımın aynı anda geçici olarak kullanılmasına da olanak tanıyarak;
  • Riverpod'u ilkesel olarak beğenmeyen ya da henüz yeterince güvenilir bulmayan kişilerin Provider'da kalabilmesi;
  • Deneysellik; Riverpod'un Provider'ın çeşitli teknik kısıtlamalarına üretime hazır çözümler arayabilmesi.

Gerçekten de Riverpod, Provider'ın manevi halefi olacak şekilde tasarlanmıştır. "Riverpod" adı da buradan gelir ("Provider" kelimesinin bir anagramıdır).

Kırıcı değişiklik

Riverpod'un tek gerçek dezavantajı, çalışması için widget tipinin değiştirilmesini gerektirmesidir:

  • StatelessWidget yerine, Riverpod ile ConsumerWidget'tan türetmelisiniz.
  • StatefulWidget yerine, Riverpod ile ConsumerStatefulWidget'tan türetmelisiniz.

Ancak bu rahatsızlık, işin bütünü içinde oldukça küçüktür. Ve bu gereklilik bir gün ortadan kalkabilir.

Doğru kütüphaneyi seçmek

Muhtemelen kendinize şunu soruyorsunuz: "Peki, bir Provider kullanıcısı olarak Provider mı yoksa Riverpod mu kullanmalıyım?".

Bu soruyu çok net biçimde yanıtlamak istiyoruz:

Muhtemelen Riverpod kullanmalısınız

Riverpod genel olarak daha iyi tasarlanmıştır ve mantığınızın çarpıcı biçimde sadeleşmesini sağlayabilir.