Riverpod 3.0'daki yenilikler
Riverpod 3.0'a hoş geldiniz!
Bu güncelleme, uzun süredir beklenen birçok özellik, hata düzeltmesi ve API sadeleştirmesi içeriyor.
Bu sürüm, daha basit ve birleşik bir Riverpod'a doğru bir geçiş dönemidir.
Bu sürüm birkaç yaşam döngüsü değişikliği içeriyor. Bunlar uygulamanızı fark edilmesi zor şekillerde bozabilir. Dikkatli yükseltin.
Geçiş rehberi için lütfen geçiş sayfasına bakın.
Öne çıkan başlıca yeniliklerden bazıları:
- Çevrimdışı kalıcılık (deneysel) - Provider'lar artık bir veritabanında kalıcı hale getirilmeyi tercih edebilir
- Mutation'lar (deneysel) - Arayüzlerin yan etkilere tepki vermesini sağlayan yeni bir mekanizma
- Otomatik yeniden deneme - Provider'lar artık başarısız olduklarında üstel geri çekilmeyle yenileniyor
Ref.mounted-BuildContext.mounted'a benzer, ancakRefiçindir.- Generic desteği (kod üretimi) - Üretilen provider'lar artık tip parametreleri tanımlayabilir
- Duraklatma/Devam ettirme desteği -
ref.listenkullanırken bir dinleyiciyi geçici olarak duraklatın - Public API'lerin birleştirilmesi - Davranışlar birleştirildi ve yinelenen arayüzler kaynaştırıldı
- Provider yaşam döngüsü değişiklikleri - Modern koda daha iyi uyum sağlaması için provider'ların davranışında küçük ayarlamalar
- Yeni test yardımcıları:
ProviderContainer.test- Bir container oluşturan ve test bittikten sonra onu otomatik olarak yok eden bir test yardımcısı.NotifierProvider.overrideWithBuild- Notifier'ın tamamının sahtesini oluşturmadan yalnızcaNotifier.buildin sahtesini oluşturmanın bir yolu.Future/StreamProvider.overrideWithValue- Eski yardımcılar geri döndüWidgetTester.container- Widget testlerinin içindeProviderContainerı elde etmek için bir yardımcı metot
- Statik olarak güvenli kapsamlama (scoping) - Eksik bir geçersiz kılmayı tespit etmek için yeni lint kuralları eklendi
Çevrimdışı kalıcılık (deneysel)
Bu özellik deneyseldir ve henüz kararlı değildir. Kullanılabilir durumdadır, ancak API büyük sürüm artışı olmadan kırıcı biçimde değişebilir.
Çevrimdışı kalıcılık, bir provider'ın cihaz üzerinde yerel olarak önbelleğe alınmasını sağlayan yeni bir özelliktir.
Böylece uygulama kapatılıp yeniden açıldığında, provider önbellekten geri yüklenebilir.
Çevrimdışı kalıcılık tercihe bağlıdır; tüm "Notifier" provider'ları tarafından
ve kod üretimi kullanıp kullanmadığınızdan bağımsız olarak desteklenir.
Riverpod yalnızca bir veritabanıyla etkileşim kurmak için gereken arayüzleri içerir. Veritabanının kendisini içermez.
Arayüzleri uyguladığı sürece istediğiniz veritabanını kullanabilirsiniz.
SQLite için resmi bir paket sürdürülmektedir: riverpod_sqflite.
Kısa bir gösterim olarak, çevrimdışı kalıcılığı şöyle kullanabilirsiniz:
- riverpod
- riverpod_generator
// Kod üretimi olmadan JsonSqFliteStorage kullanımını gösteren bir örnek.
final storageProvider = FutureProvider<JsonSqFliteStorage>((ref) async {
// SQFlite'ı başlatıyoruz. Storage örneğini provider'lar arasında paylaşmalıyız.
return JsonSqFliteStorage.open(
join(await getDatabasesPath(), 'riverpod.db'),
);
});
/// Serileştirilebilir bir Todo sınıfı.
class Todo {
const Todo({
required this.id,
required this.description,
required this.completed,
});
Todo.fromJson(Map<String, dynamic> json)
: id = json['id'] as int,
description = json['description'] as String,
completed = json['completed'] as bool;
final int id;
final String description;
final bool completed;
Map<String, dynamic> toJson() {
return {
'id': id,
'description': description,
'completed': completed,
};
}
}
final todosProvider =
AsyncNotifierProvider<TodosNotifier, List<Todo>>(TodosNotifier.new);
class TodosNotifier extends AsyncNotifier<List<Todo>>{
FutureOr<List<Todo>> build() async {
// persist'i 'build' metodumuzun başında çağırıyoruz.
// Bu şunları yapar:
// - Bu metot ilk kez çalıştığında veritabanını okur ve durumu
// kalıcı hale getirilmiş değerle günceller.
// - Bu provider'daki değişiklikleri dinler ve bunları veritabanına yazar.
persist(
// JsonSqFliteStorage örneğimizi aktarıyoruz. Future'ı "await" etmeye gerek yok.
// Riverpod bununla ilgilenecektir.
ref.watch(storageProvider.future),
// Bu durum için benzersiz bir anahtar.
// Başka hiçbir provider aynı anahtarı kullanmamalıdır.
key: 'todos',
// Varsayılan olarak durum çevrimdışı yalnızca 2 gün önbelleğe alınır.
// Önbellek süresini değiştirmek için isteğe bağlı olarak aşağıdaki satırı yorumdan çıkarabiliriz.
// options: const StorageOptions(cacheTime: StorageCacheTime.unsafe_forever),
encode: jsonEncode,
decode: (json) {
final decoded = jsonDecode(json) as List;
return decoded
.map((e) => Todo.fromJson(e as Map<String, Object?>))
.toList();
},
);
// Todo'ları sunucudan asenkron olarak çekiyoruz.
// await sürerken, kalıcı hale getirilmiş todo listesi kullanılabilir olacaktır.
// Ağ isteği tamamlandıktan sonra sunucudan gelen durum, kalıcı hale
// getirilmiş duruma göre öncelikli olacaktır.
final todos = await fetchTodos();
return todos;
}
Future<void> add(Todo todo) async {
// Durumu değiştirirken, değişikliği kalıcı hale getirmek için ekstra bir mantığa gerek yok.
// Riverpod yeni durumu otomatik olarak önbelleğe alır ve veritabanına yazar.
state = AsyncData([...await future, todo]);
}
}
Future<JsonSqFliteStorage> storage(Ref ref) async {
// SQFlite'ı başlatıyoruz. Storage örneğini provider'lar arasında paylaşmalıyız.
return JsonSqFliteStorage.open(
join(await getDatabasesPath(), 'riverpod.db'),
);
}
/// Serileştirilebilir bir Todo sınıfı. Basit serileştirme için Freezed kullanıyoruz.
abstract class Todo with _$Todo {
const factory Todo({
required int id,
required String description,
required bool completed,
}) = _Todo;
factory Todo.fromJson(Map<String, dynamic> json) => _$TodoFromJson(json);
}
()
class TodosNotifier extends _$TodosNotifier {
FutureOr<List<Todo>> build() async {
// persist'i 'build' metodumuzun başında çağırıyoruz.
// Bu şunları yapar:
// - Bu metot ilk kez çalıştığında veritabanını okur ve durumu
// kalıcı hale getirilmiş değerle günceller.
// - Bu provider'daki değişiklikleri dinler ve bunları veritabanına yazar.
persist(
// JsonSqFliteStorage örneğimizi aktarıyoruz. Future'ı "await" etmeye gerek yok.
// Riverpod bununla ilgilenecektir.
ref.watch(storageProvider.future),
// Varsayılan olarak durum çevrimdışı yalnızca 2 gün önbelleğe alınır.
// Önbellek süresini değiştirmek için isteğe bağlı olarak aşağıdaki satırı yorumdan çıkarabiliriz.
// options: const StorageOptions(cacheTime: StorageCacheTime.unsafe_forever),
);
// Todo'ları sunucudan asenkron olarak çekiyoruz.
// await sürerken, kalıcı hale getirilmiş todo listesi kullanılabilir olacaktır.
// Ağ isteği tamamlandıktan sonra sunucudan gelen durum, kalıcı hale
// getirilmiş duruma göre öncelikli olacaktır.
final todos = await fetchTodos();
return todos;
}
Future<void> add(Todo todo) async {
// Durumu değiştirirken, değişikliği kalıcı hale getirmek için ekstra bir mantığa gerek yok.
// Riverpod yeni durumu otomatik olarak önbelleğe alır ve veritabanına yazar.
state = AsyncData([...await future, todo]);
}
}
Mutation'lar (deneysel)
Bu özellik deneyseldir ve henüz kararlı değildir. Kullanılabilir durumdadır, ancak API büyük sürüm artışı olmadan kırıcı biçimde değişebilir.
Riverpod 3.0 ile "mutation" adı verilen yeni bir özellik geliyor.
Bu özellik iki sorunu çözüyor:
- Kullanıcı arayüzünün "yan etkilere" (form gönderimi, buton tıklaması vb.) tepki vermesini sağlar; böylece yükleniyor/başarılı/hata mesajları gösterebilir. "Bir form başarıyla gönderildiğinde bir toast göster" gibi düşünün.
- Ref.read ile Otomatik yok etme birlikte kullanıldığında
onPressedgeri çağırmalarının, bir yan etki hâlâ devam ederken provider'ların yok edilmesine yol açabildiği sorunu çözer.
Kısacası, yeni bir Mutation nesnesi eklendi. Tıpkı provider'lar gibi, üst düzey bir final değişken olarak tanımlanır:
final addTodoMutation = Mutation<void>();
Bundan sonra kullanıcı arayüzünüz, mutation'ların durumunu dinlemek için ref.listen/ref.watch kullanabilir:
class AddTodoButton extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
// "addTodo" yan etkisinin durumunu dinle
final addTodo = ref.watch(addTodoMutation);
return switch (addTodo) {
// Devam eden bir yan etki yok
// Bir gönder butonu gösterelim
MutationIdle() => ElevatedButton(
// Tıklandığında yan etkiyi tetikle
onPressed: () {
// TODO kod parçacığından sonraki açıklamaya bakın
},
child: const Text('Submit'),
),
// Yan etki devam ediyor. Bir spinner gösteriyoruz
MutationPending() => const CircularProgressIndicator(),
// Yan etki başarısız oldu. Bir yeniden deneme butonu gösteriyoruz
MutationError() => ElevatedButton(
onPressed: () {
// TODO kod parçacığından sonraki açıklamaya bakın
},
child: const Text('Retry'),
),
// Yan etki başarılı oldu. Bir başarı mesajı gösteriyoruz
MutationSuccess() => const Text('Todo added!'),
};
}
}
Son olarak, onPressed geri çağırmamızın içinde yan etkimizi şu şekilde
tetikleyebiliriz:
onPressed: () {
addTodoMutation.run(ref, () async {
// Yan etkimizi burada çalıştırıyoruz.
// Burada bir Notifier'ı doğrudan okuyup üzerinde bir metot çağırabiliriz.
await ref.read(todoListProvider.notifier).addTodo('New Todo');
});
}
Otomatik yeniden deneme
3.0 ile birlikte, başlatma sırasında başarısız olan provider'lar otomatik olarak yeniden denenecek. Yeniden deneme üstel geri çekilme ile yapılır ve provider başarılı olana veya yok edilene kadar yeniden denenir. Bu, bir işlem ağ bağlantısının olmaması gibi geçici bir sorun yüzünden başarısız olduğunda işinize yarar.
Varsayılan davranış her hatayı yeniden dener; 200 ms'lik bir gecikmeyle başlar ve bu gecikme
her yeniden denemede 6,4 saniyeye kadar iki katına çıkar.
Bu davranış, bir retry parametresi aktarılarak ProviderContainer/ProviderScope üzerinde tüm provider'lar için özelleştirilebilir:
- ProviderScope
- ProviderContainer
void main() {
runApp(
ProviderScope(
// Yeniden deneme mantığını özelleştirebilirsiniz; örneğin belirli
// hataları atlamak, yeniden deneme sayısına bir sınır koymak
// ya da gecikmeyi değiştirmek için
retry: (retryCount, error) {
if (error is SomeSpecificError) return null;
if (retryCount > 5) return null;
return Duration(seconds: retryCount * 2);
},
child: MyApp(),
),
);
}
void main() {
final container = ProviderContainer(
// Yeniden deneme mantığını özelleştirebilirsiniz; örneğin belirli
// hataları atlamak, yeniden deneme sayısına bir sınır koymak
// ya da gecikmeyi değiştirmek için
retry: (retryCount, error) {
if (error is SomeSpecificError) return null;
if (retryCount > 5) return null;
return Duration(seconds: retryCount * 2);
},
);
}
Alternatif olarak bu, provider yapıcısına bir retry parametresi aktarılarak provider bazında da yapılandırılabilir:
- riverpod
- riverpod_generator
final todoListProvider = NotifierProvider<TodoList, List<Todo>>(
TodoList.new,
retry: (retryCount, error) {
if (error is SomeSpecificError) return null;
if (retryCount > 5) return null;
return Duration(seconds: retryCount * 2);
},
);
Duration retry(int retryCount, Object error) {
if (error is SomeSpecificError) return null;
if (retryCount > 5) return null;
return Duration(seconds: retryCount * 2);
}
(retry: retry)
class TodoList extends _$TodoList {
List<Todo> build() => [];
}
Ref.mounted
Uzun zamandır beklenen Ref.mounted nihayet burada! BuildContext.mounted'a benzer, ancak Ref içindir.
Bir asenkron işlemin ardından provider'ın hâlâ mounted olup olmadığını kontrol etmek için kullanabilirsiniz:
- riverpod
- riverpod_generator
class TodoList extends Notifier<List<Todo>> {
List<Todo> build() => [];
Future<void> addTodo(String title) async {
// Yeni todo'yu sunucuya gönder
final newTodo = await api.addTodo(title);
// Asenkron işlemden sonra provider'ın hâlâ
// mounted olup olmadığını kontrol et
if (!ref.mounted) return;
// Mounted ise durumu güncelle
state = [...state, newTodo];
}
}
class TodoList extends _$TodoList {
List<Todo> build() => [];
Future<void> addTodo(String title) async {
// Yeni todo'yu sunucuya gönder
final newTodo = await api.addTodo(title);
// Asenkron işlemden sonra provider'ın hâlâ
// mounted olup olmadığını kontrol et
if (!ref.mounted) return;
// Mounted ise durumu güncelle
state = [...state, newTodo];
}
}
Bunun çalışabilmesi için epeyce yaşam döngüsü değişikliği gerekti.
Yaşam döngüsü değişiklikleri bölümünü mutlaka okuyun.
Generic desteği (kod üretimi)
Kod üretimi kullanırken artık üretilen provider'larınız için tip parametreleri tanımlayabilirsiniz. Tip parametreleri diğer provider parametreleri gibi çalışır ve provider dinlenirken aktarılmaları gerekir.
T multiply<T extends num>(T a, T b) {
return a * b;
}
// ...
int integer = ref.watch(multiplyProvider<int>(2, 3));
double decimal = ref.watch(multiplyProvider<double>(2.5, 3.5));
Duraklatma/Devam ettirme desteği
2.0'da Riverpod bir ölçüde duraklatma/devam ettirme desteğine zaten sahipti, ancak bu oldukça sınırlıydı.
3.0 ile birlikte tüm ref.listen dinleyicileri istendiğinde elle duraklatılıp devam ettirilebiliyor:
final subscription = ref.listen(
todoListProvider,
(previous, next) {
// Yeni değerle bir şeyler yap
},
);
subscription.pause();
subscription.resume();
Aynı zamanda Riverpod artık çeşitli durumlarda provider'ları duraklatıyor:
- Bir provider artık görünür olmadığında duraklatılır (TickerMode temel alınarak).
- Bir provider yeniden oluşturulduğunda, abonelikleri yeniden oluşturma tamamlanana kadar duraklatılır.
- Bir provider duraklatıldığında, tüm abonelikleri de duraklatılır.
Daha fazla ayrıntı için yaşam döngüsü değişiklikleri bölümüne bakın.
Public API'lerin birleştirilmesi
Riverpod 3.0'ın hedeflerinden biri API'yi sadeleştirmektir. Buna şunlar dahildir:
- Neyin önerildiğini, neyin önerilmediğini öne çıkarmak
- Gereksiz arayüz tekrarlarını kaldırmak
- Tüm işlevlerin tutarlı biçimde çalışmasını sağlamak
Bu amaçla birkaç değişiklik yapıldı:
[StateProvider]/[StateNotifierProvider] ve [ChangeNotifierProvider] artık önerilmiyor ve farklı bir import'a taşındı
Bu provider'lar kaldırılmadı, yalnızca farklı bir import'a taşındı. Şunun yerine:
import 'package:riverpod/riverpod.dart';
Artık şunu kullanmalısınız:
import 'package:riverpod/legacy.dart';
Bu, söz konusu provider'ların artık önerilmediğini vurgulamak içindir.
Aynı zamanda geriye dönük uyumluluk için korunuyorlar.
AutoDispose arayüzleri kaldırıldı
Hayır, "auto-dispose" özelliği kaldırılmadı. Bu yalnızca arayüzlerle ilgili.
2.0'da tüm provider'lar, Ref'ler ve Notifier'lar auto-dispose uğruna ikiye katlanmıştı (
Ref ile AutoDisposeRef, Notifier ile AutoDisposeNotifier gibi).
Bu, bazı uç durumlarda derleme hatası alabilmek için yapılmıştı; ancak bedeli
daha kötü bir API oldu.
3.0'da arayüzler birleştirildi ve önceki derleme hatası artık bir lint kuralı olarak
uygulanıyor (riverpod_lint ile).
Somut olarak bu şu demek: AutoDisposeNotifier referanslarının tümünü Notifier ile
değiştirebilirsiniz. Kodunuzun davranışı değişmemeli.
final provider = NotifierProvider.autoDispose<MyNotifier, int>(
MyNotifier.new,
);
- class MyNotifier extends AutoDisposeNotifier<int> {
+ class MyNotifier extends Notifier<int> {
}
"FamilyNotifier" ve "Notifier" birleştirildi
Önceki maddeye benzer şekilde, FamilyNotifier ve Notifier arayüzleri
artık birleştirildi.
Kısacası, şunun yerine:
final provider = NotifierProvider.family<CounterNotifier, int, Argument>(
MyNotifier.new,
);
class CounterNotifier extends FamilyNotifier<int, Argument> {
int build(Argument arg) => 0;
}
Artık şunu yapıyoruz:
final provider = NotifierProvider.family<CounterNotifier, int, Argument>(
CounterNotifier.new,
);
class CounterNotifier extends Notifier<int> {
CounterNotifier(this.arg);
final Argument arg;
int build() => 0;
}
Yani Notifier+FamilyNotifier+AutoDisposeNotifier+AutoDisposeFamilyNotifier yerine
her zaman Notifier sınıfını kullanıyoruz.
Bu değişikliğin kod üretimine hiçbir etkisi yoktur.
Hepsini yönetecek tek bir Ref
Riverpod 2.0'da her provider kendi Ref alt sınıfıyla geliyordu (FutureProviderRef, StreamProviderRef vb.).
Bazı Reflerin state özelliği, bazılarının future veya notifier özelliği vardı.
Faydalı olsa da, bu kadar karmaşıklık pek bir kazanç sağlamıyordu. Bunun nedenlerinden biri,
Notifier'ların sahip olduğu ek özelliklere zaten sahip olmasıydı;
dolayısıyla arayüzler gereksiz tekrar ediyordu.
3.0'da Ref birleştirildi. Artık Ref<T> gibi bir generic parametre yok,
FutureProviderRef de yok. Tek bir şeyimiz var: Ref.
Pratikte bu, üretilen provider'ların sözdiziminin sadeleşmesi anlamına gelir:
-Example example(ExampleRef ref) {
+Example example(Ref ref) {
return Example();
}
Artık tüm updateShouldNotify çağrıları == kullanıyor
updateShouldNotify, bir durum değişikliği olduğunda bir provider'ın dinleyicilerine
bildirim göndermesi gerekip gerekmediğini belirlemek için kullanılan bir metottur.
Ancak 2.0'da bu metodun uygulanışı provider'dan provider'a epey değişiyordu.
Bazı provider'lar ==, bazıları identical, bazıları da daha karmaşık bir mantık kullanıyordu.
3.0'dan itibaren tüm provider'lar bildirimleri filtrelemek için == kullanır.
Bu sizi birkaç şekilde etkileyebilir:
- Bazı provider'larınız belirli durumlarda artık dinleyicilerine bildirim göndermeyebilir.
- Bazı dinleyiciler öncekinden daha sık bildirim alabilir.
==ı geçersiz kılan büyük bir veri sınıfınız varsa, küçük bir performans etkisi görebilirsiniz.
Etkileneceğiniz en yaygın durum StreamProvider/StreamNotifier kullanımıdır;
çünkü stream'in olayları artık == ile filtreleniyor.
Bu değişikliklerden etkileniyorsanız, özel bir uygulama kullanmak için
updateShouldNotifyı geçersiz kılabilirsiniz:
- riverpod
- riverpod_generator
class TodoList extends StreamNotifier<Todo> {
Stream<Todo> build() => Stream(...);
bool updateShouldNotify(AsyncValue<Todo> previous, AsyncValue<Todo> next) {
// Özel uygulama
return true;
}
}
class TodoList extends _$TodoList {
Stream<Todo> build() => Stream(...);
bool updateShouldNotify(AsyncValue<Todo> previous, AsyncValue<Todo> next) {
// Özel uygulama
return true;
}
}
Provider yaşam döngüsü değişiklikleri
Ref'ler ve Notifier'lar yok edildikten sonra artık kullanılamaz
2.0'da bazı uç durumlarda Ref veya Notifier gibi şeylerle yok edildikten sonra bile etkileşime geçebiliyordunuz. Bu istenen bir durum değildi ve çeşitli ciddi hatalara yol açıyordu.
3.0'da, yok edilmiş bir Ref/Notifier ile etkileşime geçmeye çalışırsanız Riverpod bir hata fırlatır.
Bir Ref/Notifier'ın hâlâ kullanılabilir olup olmadığını kontrol etmek için [Ref.mounted] kullanabilirsiniz.
final provider = FutureProvider<int>((ref) async {
await Future.delayed(Duration(seconds: 1));
// await sırasında yok edildiyse provider'ı iptal et.
// İstediğiniz her şeyi fırlatabilir ve bu istisnayı hata raporlama araçlarınızda göz ardı edebilirsiniz.
if (!ref.mounted) throw MyException();
return 42;
});
Bir provider'ı okumak istisnayla sonuçlandığında, hata artık bir ProviderException içine sarılıyor
Önceden, bir provider hata fırlattığında Riverpod bazen o hatayı doğrudan yeniden fırlatıyordu:
- riverpod
- riverpod_generator
final exampleProvider = FutureProvider<int>((ref) async {
throw StateError('Error');
});
// ...
ElevatedButton(
onPressed: () async {
// Bu, StateError'ı yeniden fırlatır
ref.read(exampleProvider).requireValue;
// Bu da StateError'ı yeniden fırlatır
await ref.read(exampleProvider.future);
},
child: Text('Click me'),
);
Future<int> example(Ref ref) async {
throw StateError('Error');
}
// ...
ElevatedButton(
onPressed: () async {
// Bu, StateError'ı yeniden fırlatır
ref.read(exampleProvider).requireValue;
// Bu da StateError'ı yeniden fırlatır
await ref.read(exampleProvider.future);
},
child: Text('Click me'),
);
3.0'da bu değişti. Bunun yerine hata, hem orijinal hatayı hem de yığın izini (stack trace)
içeren bir ProviderException içine sarılır.
AsyncValue.error, ref.listen(..., onError: ...) ve ProviderObserver'lar bu değişiklikten etkilenmez
ve hatayı değiştirilmemiş biçimde almaya devam eder.
Bunun birden fazla faydası var:
- Çok daha iyi bir yığın izine sahip olduğumuz için hata ayıklama iyileşir
- Artık bir provider'ın kendisinin mi başarısız olduğunu, yoksa başarısız olan başka bir provider'a bağlı olduğu için mi hata durumunda olduğunu belirlemek mümkün.
Örneğin bir ProviderObserver, aynı hatayı iki kez günlüğe kaydetmemek için bunu kullanabilir:
class MyObserver extends ProviderObserver {
void providerDidFail(ProviderObserverContext context, Object error, StackTrace stackTrace) {
if (error is ProviderException) {
// Provider doğrudan başarısız olmadı; başarısız olan bir provider'a bağlı.
// Dolayısıyla hata zaten günlüğe kaydedilmişti.
return;
}
// Hatayı günlüğe kaydet
print('Provider failed: $error');
}
}
Bu, Riverpod'un otomatik yeniden deneme mekanizmasında dahili olarak kullanılır. Varsayılan otomatik yeniden deneme
ProviderExceptionları göz ardı eder:
ProviderContainer(
// Varsayılan yeniden deneme davranışının örneği
retry: (retryCount, error) {
if (error is ProviderException) return null;
// ...
},
);
Görünür olmayan widget'ların içindeki dinleyiciler artık duraklatılıyor
Riverpod'un artık dinleyicileri duraklatmanın bir yolu olduğuna göre, Riverpod bunu widget görünür değilken dinleyicileri doğal olarak duraklatmak için kullanıyor. Pratikte bu şu demek: Görünür widget ağacı tarafından kullanılmayan provider'lar duraklatılır.
Somut bir örnek olarak, iki rotası olan bir uygulama düşünün:
- Bir provider aracılığıyla bir websocket dinleyen bir ana sayfa
- O websocket'e ihtiyaç duymayan bir ayarlar sayfası
Tipik uygulamalarda kullanıcı önce ana sayfayı açar ve ardından ayarlar sayfasını açar. Yani ayarlar sayfası açıkken ana sayfa da açıktır, ancak görünür değildir.
2.0'da ana sayfa websocket'i etkin biçimde dinlemeye devam ederdi.
3.0'da ise websocket provider'ı duraklatılacak ve muhtemelen kaynak tasarrufu sağlanacaktır.
Nasıl çalışır:
Riverpod, bir widget'ın görünür olup olmadığını belirlemek için TickerMode'a dayanır. Değer
false olduğunda bir Consumer'ın tüm dinleyicileri duraklatılır.
Bu ayrıca, consumer'larınızın duraklama davranışını elle kontrol etmek için TickerMode'a kendiniz de dayanabileceğiniz anlamına gelir. Dinleyicileri zorla devam ettirmek/duraklatmak için değeri isteğinize göre true/false yapabilirsiniz:
class MyWidget extends StatelessWidget {
Widget build(BuildContext context) {
return TickerMode(
enabled: false, // Bu, dinleyicileri duraklatacaktır
child: Consumer(
builder: (context, ref, child) {
// Bu "watch", TickerMode true yapılana kadar
// duraklatılmış olacaktır
final value = ref.watch(myProvider);
return Text(value.toString());
},
),
);
}
}
Bir provider yalnızca duraklatılmış provider'lar tarafından kullanılıyorsa, o da duraklatılır
Riverpod 2.0'da zaten bir tür duraklat/devam et desteği vardı. Ancak sınırlıydı ve bazı
uç durumları kapsamakta yetersiz kalıyordu.
Şunu düşünün:
- riverpod
- riverpod_generator
final exampleProvider = Provider<int>((ref) {
ref.onCancel(() => print('paused'));
ref.onResume(() => print('resumed'));
return 0;
});
int example(Ref ref) {
ref.keepAlive();
ref.onCancel(() => print('paused'));
ref.onResume(() => print('resumed'));
return 0;
}
2.0'da bu provider üzerinde bir kez ref.read çağırsaydınız,
provider'ın durumu korunurdu, ancak 'paused' yazdırılırdı. Bunun nedeni,
ref.read çağırmanın provider'ı "dinlememesidir". Provider "dinlenmediği" için de
duraklatılır.
Bu, o anda kullanılmayan provider'ları duraklatmak için faydalıdır!
Sorun şu ki, birçok durumda bu optimizasyon işe yaramıyor.
Örneğin provider'ınız başka bir provider aracılığıyla dolaylı olarak kullanılıyor olabilir.
- riverpod
- riverpod_generator
final anotherProvider = Provider<int>((ref) {
return ref.watch(exampleProvider);
});
class MyWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
return Button(
onPressed: () {
ref.read(anotherProvider);
},
child: Text('Click me'),
);
}
}
int another(Ref ref) {
ref.keepAlive();
return ref.watch(exampleProvider);
}
class MyWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
return Button(
onPressed: () {
ref.read(anotherProvider);
},
child: Text('Click me'),
);
}
}
Bu senaryoda, butona bir kez tıklarsak
anotherProvider bizim exampleProviderımızı dinlemeye başlayacaktır. Ancak anotherProvider
artık kullanılmadığı için duraklatılacaktır. Yine de exampleProvider duraklatılmayacaktır,
çünkü hâlâ kullanıldığını sanmaktadır.
Bu yüzden butona tıklamak artık 'paused' yazdırmayacaktır.
3.0'da bu düzeltildi. Bir provider yalnızca duraklatılmış provider'lar tarafından kullanılıyorsa, o da duraklatılır.
Bir provider yeniden oluşturulduğunda, önceki abonelikleri artık yeniden oluşturma tamamlanana kadar korunuyor
2.0'da, asenkron provider'lar 'auto-dispose' ile birlikte kullanıldığında bilinen bir rahatsızlık vardı.
Özellikle, bir asenkron provider bir await sonrasında auto-dispose bir provider'ı izlediğinde,
"auto dispose" beklenmedik şekilde tetiklenebiliyordu.
Şunu düşünün:
- riverpod
- riverpod_generator
final autoDisposeProvider = StreamProvider.autoDispose<int>((ref) {
ref.onDispose(() => print('disposed'));
ref.onCancel(() => print('paused'));
ref.onResume(() => print('resumed'));
// Her saniye bir değer yayan bir stream
return Stream.periodic(Duration(seconds: 1), (i) => i);
});
final asynchronousExampleProvider = FutureProvider<int>((ref) async {
print('Before async gap');
// Bir provider içindeki asenkron boşluk; genellikle bir API çağrısı.
// Bu, asenkron işlem tamamlanmadan önce "autoDispose"
// provider'ını yok edecektir
await null;
print('after async gap');
// Auto-dispose provider'ımızı asenkron işlemden
// sonra dinliyoruz
return ref.watch(autoDisposeProvider.future);
});
void main() {
final container = ProviderContainer();
// Bu, her saniye 'disposed' yazdıracak
// ve sürekli 0 yazdıracaktır
container.listen(asynchronousExampleProvider, (_, value) {
if (value is AsyncData) print('${value.value}\n----');
});
}
Stream<int> autoDispose(Ref ref) {
ref.onDispose(() => print('disposed'));
ref.onCancel(() => print('paused'));
ref.onResume(() => print('resumed'));
// Her saniye bir değer yayan bir stream
return Stream.periodic(Duration(seconds: 1), (i) => i);
}
Future<int> asynchronousExample(Ref ref) async {
print('Before async gap');
// Bir provider içindeki asenkron boşluk; genellikle bir API çağrısı.
// Bu, asenkron işlem tamamlanmadan önce "autoDispose"
// provider'ını yok edecektir
await null;
print('after async gap');
// Auto-dispose provider'ımızı asenkron işlemden
// sonra dinliyoruz
return ref.watch(autoDisposeProvider.future);
}
void main() {
final container = ProviderContainer();
// Bu, her saniye 'disposed' yazdıracak
// ve sürekli 0 yazdıracaktır
container.listen(asynchronousExampleProvider, (_, value) {
if (value is AsyncData) print('${value.value}\n----');
});
}
Bunu Dartpad üzerinde çalıştırırsanız şunları yazdırdığını görürsünüz:
// İlk yazdırma
Before async gap
after async gap
0
---- // İkinci ve sonraki yazdırmalar
paused
Before async gap
disposed // 'autoDispose' provider'ı asenkron boşluk sırasında yok edildi!
after async gap
0
----
paused
Before async gap
disposed
after async gap
0
----
... // Ve her saniye böyle devam eder
Gördüğünüz gibi bu, her saniye tutarlı biçimde 0 yazdırıyor;
çünkü autoDispose provider'ı asenkron boşluk sırasında tekrar tekrar yok ediliyor.
Geçici bir çözüm, ref.watch çağrısını await ifadesinden önceye taşımaktı.
Ancak bu hataya açık, pek sezgisel değil ve her zaman da mümkün değil.
3.0'da bu, dinleyicilerin yok edilmesi geciktirilerek düzeltildi.
Bir provider yeniden oluşturulduğunda, tüm dinleyicilerini hemen kaldırmak yerine
onları duraklatır.
Tamamen aynı kod artık şunu yazdıracak:
// İlk yazdırma
Before async gap
after async gap
0
----
paused
Before async gap
after async gap
resumed
1
----
paused
Before async gap
after async gap
resumed
2
----
... // Ve her saniye böyle devam eder
Provider'lardaki istisnalar ProviderException olarak yeniden fırlatılır.
"Bir provider başarısız oldu" ile "bir provider başarısız olan bir provider'a bağlı" durumlarını ayırt edebilmek adına,
Riverpod 3.0 artık istisnaları, orijinalini içeren bir ProviderException içine sarar.
Yani provider'larınızda hataları yakalıyorsanız, ProviderExceptionın içeriğini incelemek için
try/catch bloklarınızı güncellemeniz gerekecek:
try {
ref.watch(failingProvider);
} on ProviderException catch (e) {
switch (e.exception) {
case SomeSpecificError():
// Belirli hatayı işle
default:
// Diğer hataları işle
rethrow;
}
}
Yeni test yardımcıları
ProviderContainer.test
2.0'da tipik test kodu, elle yazılmış createContainer adlı bir yardımcıya dayanırdı.
3.0'da bu yardımcı artık Riverpod'un bir parçası ve adı ProviderContainer.test.
Yeni bir container oluşturur ve test bittikten sonra onu otomatik olarak yok eder.
void main() {
test('My test', () {
final container = ProviderContainer.test();
// Container'ı kullanın
// ...
// Container, test bittikten sonra otomatik olarak yok edilir
});
}
createContainer ifadesini ProviderContainer.test ile global olarak arayıp değiştirmeniz güvenlidir.
NotifierProvider.overrideWithBuild
Artık notifier'ın tamamının sahtesini oluşturmadan, yalnızca Notifier.build metodunun sahtesini oluşturmak mümkün.
Bu, notifier'ınızı belirli bir durumla başlatmak isteyip yine de notifier'ın
orijinal uygulamasını kullanmak istediğinizde işe yarar.
- riverpod
- riverpod_generator
class MyNotifier extends Notifier<int> {
int build() => 0;
void increment() {
state++;
}
}
final myProvider = NotifierProvider<MyNotifier, int>(MyNotifier.new);
void main() {
final container = ProviderContainer.test(
overrides: [
myProvider.overrideWithBuild((ref, self) {
// 42'den başlaması için build metodunun sahtesini oluştur.
// "increment" metodu bundan etkilenmez.
return 42;
}),
],
);
}
class MyNotifier extends _$MyNotifier {
int build() => 0;
void increment() {
state++;
}
}
void main() {
final container = ProviderContainer.test(
overrides: [
myProvider.overrideWithBuild((ref, self) {
// 42'den başlaması için build metodunun sahtesini oluştur.
// "increment" metodu bundan etkilenmez.
return 42;
}),
],
);
}
Future/StreamProvider.overrideWithValue
Bir süre önce FutureProvider.overrideWithValue ve StreamProvider.overrideWithValue
Riverpod'dan "geçici olarak" kaldırılmıştı.
Sonunda geri döndüler!
- riverpod
- riverpod_generator
final myFutureProvider = FutureProvider<int>((ref) async {
return 42;
});
void main() {
final container = ProviderContainer.test(
overrides: [
// Provider'ı bir değerle başlatır.
// Geçersiz kılmayı değiştirmek değeri günceller.
myFutureProvider.overrideWithValue(AsyncValue.data(42)),
],
);
}
Future<int> myFutureProvider() async {
return 42;
}
void main() {
final container = ProviderContainer.test(
overrides: [
// Provider'ı bir değerle başlatır.
// Geçersiz kılmayı değiştirmek değeri günceller.
myFutureProvider.overrideWithValue(AsyncValue.data(42)),
],
);
}
WidgetTester.container
Widget ağacınızdaki ProviderContainera erişmenin basit bir yolu.
void main() {
testWidgets('can access a ProviderContainer', (tester) async {
await tester.pumpWidget(const ProviderScope(child: MyWidget()));
ProviderContainer container = tester.container();
});
}
Daha fazla bilgi için WidgetTester.container eklentisine bakın.
Özel ProviderListenable'lar
Riverpod 3.0'da artık özel ProviderListenable'lar oluşturmak mümkün. Bu, CustomProviderListenable sınıfından türeterek yapılabilir.
Aşağıdaki örnek, geri çağırmanın seçilen değer yerine bir boolean döndürdüğü,
provider.selectin bir çeşidini uygular.
final class Where<T> extends CustomProviderListenable<T, T> {
Where(this.source, this.where);
final ProviderListenable<T> source;
final bool Function(T previous, T value) where;
_WhereTransformer<T> createTransformer() => _WhereTransformer<T>();
}
final class _WhereTransformer<T>
extends SyncProviderTransformer2<T, T, Where<T>> {
T initState() => sourceState.requireValue;
void onEvent(
ProviderTransformer2<T, T, Where<T>> self,
AsyncResult<T> prev,
AsyncResult<T> next,
) {
if (listenable.where(prev.requireValue, next.requireValue)) {
state = next;
}
}
}
extension<T> on ProviderListenable<T> {
ProviderListenable<T> where(
bool Function(T previous, T value) where,
) => Where<T>(this, where);
}
Şu şekilde kullanılır: ref.watch(provider.where((previous, value) => value > 0)).
Statik olarak güvenli kapsamlama (yalnızca kod üretimi)
Riverpod artık riverpod_lint aracılığıyla, kapsamlamanın hatalı kullanıldığı durumları tespit etmenin bir yolunu içeriyor. Bu lint, çalışma zamanı hatalarını önlemek için eksik bir geçersiz kılmayı tespit eder.
Şunu düşünün:
// Tipik bir "kapsamlanmış provider"
(dependencies: [])
Future<int> myFutureProvider() => throw UnimplementedError();
Bu provider'ı kullanmak için iki seçeneğiniz var.
Aşağıdaki seçeneklerden hiçbiri kullanılmazsa, provider çalışma zamanında bir hata fırlatacaktır.
- Kullanmadan önce provider'ı
ProviderScopeile geçersiz kılın:class MyWidget extends StatelessWidget {
Widget build(BuildContext context) {
return ProviderScope(
overrides: [
myFutureProvider.overrideWithValue(AsyncValue.data(42)),
],
// Geçersiz kılınmış provider'a erişmek için bir consumer gereklidir
child: Consumer(
builder: (context, ref, child) {
// Provider'ı kullan
final value = ref.watch(myFutureProvider);
return Text(value.toString());
},
),
);
}
} - Ya da kapsamlanmış provider'ı kullanan şeyin üzerinde, ona bağımlı olduğunu belirtmek için
@Dependenciestanımlayın.([myFuture])
class MyWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
// Provider'ı kullan
final value = ref.watch(myFutureProvider);
return Text(value.toString());
}
}@Dependenciestanımlandıktan sonra,MyWidgetın tüm kullanımları yukarıdakiyle aynı iki seçeneği gerektirecektir:- Ya
MyWidgetı kullanmadan önce provider'ıProviderScopeile geçersiz kılınvoid main() {
runApp(
ProviderScope(
overrides: [
myFutureProvider.overrideWithValue(AsyncValue.data(42)),
],
child: MyWidget(),
),
);
} - Ya da
MyWidgetı kullanan şeyin üzerinde, ona bağımlı olduğunu belirtmek için@Dependenciestanımlayın.([myFuture])
class MyApp extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
// MyApp, MyWidget üzerinden dolaylı olarak kapsamlanmış provider'lar kullanıyor
return MyWidget();
}
}
- Ya
Diğer değişiklikler
AsyncValue
AsyncValue çeşitli değişiklikler aldı.
- Artık "sealed". Bu, kapsamlı desen eşlemeyi mümkün kılar:
AsyncValue<int> value;
switch (value) {
case AsyncData():
print('data');
case AsyncError():
print('error');
case AsyncLoading():
print('loading');
// default durumuna gerek yok
} valueOrNull,valueolarak yeniden adlandırıldı. Eskivaluekaldırıldı; çünkü hatalarla ilgili davranışı tuhaftı. Geçiş içinvalueOrNull->valueşeklinde global bir arayıp değiştirme yapın.AsyncValue.isFromCacheeklendi.
Bu bayrak, bir değer çevrimdışı kalıcılık üzerinden elde edildiğinde ayarlanır. Kullanıcı arayüzünüzün veritabanından gelen durum ile sunucudan gelen durumu ayırt etmesini sağlar.AsyncLoadingüzerinde isteğe bağlı birprogressözelliği mevcut. Bu, provider'larınızın bir istek için geçerli ilerlemeyi tanımlamasını sağlar:- riverpod
- riverpod_generator
class MyNotifier extends AsyncNotifier<User> {
Future<User> build() async {
// AsyncLoading'e isteğe bağlı olarak bir "progress" geçebilirsiniz
state = AsyncLoading(progress: .0);
await fetchSomething();
state = AsyncLoading(progress: 0.5);
return User();
}
}
class MyNotifier extends _$MyNotifier {
Future<User> build() async {
// AsyncLoading'e isteğe bağlı olarak bir "progress" geçebilirsiniz
state = AsyncLoading(progress: .0);
await fetchSomething();
state = AsyncLoading(progress: 0.5);
return User();
}
}
Tüm Ref dinleyicileri artık dinleyiciyi kaldırmanın bir yolunu döndürüyor
Artık çeşitli yaşam döngüsü dinleyicilerinden "aboneliği kaldırmak" mümkün:
- riverpod
- riverpod_generator
final exampleProvider = FutureProvider<int>((ref) {
// onDispose ve diğer yaşam döngüsü dinleyicileri, dinleyiciyi kaldırmak
// için bir fonksiyon döndürür.
final removeListener = ref.onDispose(() => print('dispose));
// Dinleyiciyi kaldırmak için fonksiyonu çağırmanız yeterli:
removeListener();
// ...
});
Future<int> example(Ref ref) {
// onDispose ve diğer yaşam döngüsü dinleyicileri, dinleyiciyi kaldırmak
// için bir fonksiyon döndürür.
final removeListener = ref.onDispose(() => print('dispose));
// Dinleyiciyi kaldırmak için fonksiyonu çağırmanız yeterli:
removeListener();
// ...
}
Zayıf dinleyiciler - otomatik yok etmeyi engellemeden bir provider'ı dinlemek.
Ref.listen kullanırken isteğe bağlı olarak weak: true belirtebilirsiniz:
- riverpod
- riverpod_generator
final exampleProvider = FutureProvider<int>((ref) {
ref.listen(
anotherProvider,
// Bayrağı belirtin
weak: true,
(previous, next) {},
);
// ...
});
Future<int> example(Ref ref) {
ref.listen(
anotherProvider,
// Bayrağı belirtin
weak: true,
(previous, next) {},
);
// ...
}
Bu bayrağı belirtmek, dinlenen provider kullanılmayı bıraktığında Riverpod'un onu yine de yok edebileceğini söyler.
Bu bayrak, tek bir provider içinde birden fazla "doğruluk kaynağı"nı birleştirmeye ilişkin bazı özel kullanım senaryolarına yardımcı olan ileri düzey bir özelliktir.