`StateNotifier`'dan geçiş
Riverpod 2.0 ile birlikte yeni sınıflar
tanıtıldı: Notifier / AsyncNotifier.
Artık bu yeni API'ler tercih edilerek StateNotifier kullanımı önerilmiyor.
Bu sayfa, kullanımdan kaldırılan StateNotifier'dan yeni API'lere nasıl geçileceğini gösteriyor.
AsyncNotifier'ın getirdiği başlıca fayda daha iyi bir async desteğidir; gerçekten de
AsyncNotifier, kullanıcı arayüzünden değiştirilme yolları sunabilen bir FutureProvider gibi düşünülebilir.
Ayrıca yeni (Async)Notifier'lar:
- Sınıflarının içinde bir
Refnesnesi sunar - Kod üretimi kullanan ve kullanmayan yaklaşımlar arasında benzer bir sözdizimi sunar
- Senkron ve asenkron sürümleri arasında benzer bir sözdizimi sunar
- Mantığı Provider'lardan uzaklaştırıp Notifier'ların kendisinde toplar
Şimdi bir Notifier'ın nasıl tanımlandığına, StateNotifier ile nasıl karşılaştırıldığına ve asenkron
durum için yeni AsyncNotifier'a nasıl geçileceğine bakalım.
Yeni sözdizimi karşılaştırması
Bu karşılaştırmaya dalmadan önce bir Notifier'ın nasıl tanımlandığını bildiğinizden emin olun.
Bkz. Provider'lar.
Eski StateNotifier sözdizimini kullanarak bir örnek yazalım:
class CounterNotifier extends StateNotifier<int> {
CounterNotifier() : super(0);
void increment() => state++;
void decrement() => state--;
}
final counterNotifierProvider =
StateNotifierProvider<CounterNotifier, int>((ref) {
return CounterNotifier();
});
İşte kabaca aynı anlama gelen, yeni Notifier API'leriyle yazılmış aynı örnek:
- riverpod
- riverpod_generator
class CounterNotifier extends Notifier<int> {
int build() => 0;
void increment() => state++;
void decrement() => state++;
}
final counterNotifierProvider = NotifierProvider<CounterNotifier, int>(CounterNotifier.new);
class CounterNotifier extends _$CounterNotifier {
int build() => 0;
void increment() => state++;
void decrement() => state--;
}
Notifier ile StateNotifier karşılaştırıldığında şu temel farklar gözlemlenebilir:
StateNotifier'ın tepkisel bağımlılıkları kendi provider'ında tanımlanırken,Notifierbu mantığıbuildmetodunda toplarStateNotifier'ın tüm başlatma süreci provider'ı ile yapıcısı arasında bölünmüşken,Notifierböyle bir mantık için tek bir yer ayırırStateNotifier'ın aksine birNotifier'ın yapıcısına hiçbir zaman mantık yazılmadığına dikkat edin
Benzer sonuçlar, Notifier'ın asenkron eşdeğeri olan AsyncNotifier için de çıkarılabilir.
Asenkron StateNotifier'lardan geçiş
Yeni API sözdiziminin başlıca cazibesi, asenkron verilerde geliştirilmiş bir geliştirici deneyimidir.
Şu örneği ele alalım:
class AsyncTodosNotifier extends StateNotifier<AsyncValue<List<Todo>>> {
AsyncTodosNotifier() : super(const AsyncLoading()) {
_postInit();
}
Future<void> _postInit() async {
state = await AsyncValue.guard(() async {
final json = await http.get('api/todos');
return [...json.map(Todo.fromJson)];
});
}
// ...
}
İşte yukarıdaki örneğin yeni AsyncNotifier API'leriyle yeniden yazılmış hâli:
- riverpod
- riverpod_generator
class AsyncTodosNotifier extends AsyncNotifier<List<Todo>> {
FutureOr<List<Todo>> build() async {
final json = await http.get('api/todos');
return [...json.map(Todo.fromJson)];
}
// ...
}
final asyncTodosNotifier =
AsyncNotifierProvider<AsyncTodosNotifier, List<Todo>>(
AsyncTodosNotifier.new,
);
class AsyncTodosNotifier extends _$AsyncTodosNotifier {
FutureOr<List<Todo>> build() async {
final json = await http.get('api/todos');
return [...json.map(Todo.fromJson)];
}
// ...
}
AsyncNotifier de tıpkı Notifier gibi daha basit ve daha tutarlı bir API getirir.
Burada AsyncNotifier'ı metotları olan bir FutureProvider olarak görmek kolaydır.
AsyncNotifier, StateNotifier'da bulunmayan bir dizi yardımcı araç ve getter ile gelir; örneğin
future
ve update gibi.
Bu, asenkron mutation'ları ve yan etkileri ele alırken çok daha basit bir mantık yazmamızı sağlar.
Ayrıca bkz. Provider'lar.
StateNotifier<AsyncValue<T>>'dan AsyncNotifier<T>'a geçiş şuna indirgenir:
- Başlatma mantığını
buildiçine koymak - Başlatmadaki veya yan etki metotlarındaki tüm
catch/trybloklarını kaldırmak buildiçindeki tümAsyncValue.guardkullanımlarını kaldırmak; çünkü o,Future'larıAsyncValue'lara dönüştürür
Avantajlar
Bu birkaç örnekten sonra, şimdi Notifier ve AsyncNotifier'ın başlıca avantajlarını öne çıkaralım:
- Yeni sözdizimi, özellikle asenkron durum için çok daha basit ve okunabilir hissettirmeli
- Yeni API'ler genel olarak daha az standart şablon kod içerme eğilimindedir
- Sözdizimi artık, yazdığınız provider'ın tipi ne olursa olsun birleştirilmiştir ve bu da kod üretimini mümkün kılar (bkz. Kod üretimi hakkında)
Hadi daha da ilerleyip başka farkları ve benzerlikleri öne çıkaralım.
Açık .family ve .autoDispose değişiklikleri
Bir diğer önemli fark, yeni API'lerde family'lerin ve otomatik yok etmenin nasıl ele alındığıdır.
Değişiklikler sınıfın içinde açıkça belirtilir; tüm parametreler doğrudan build metoduna
enjekte edilir, böylece başlatma mantığı için kullanılabilir olurlar.
Bu, daha iyi okunabilirlik, daha fazla özlülük ve genel olarak daha az hata getirmelidir.
Bir StateNotifierProvider.familynin tanımlandığı şu örneği ele alalım.
class BugsEncounteredNotifier extends StateNotifier<AsyncValue<int>> {
BugsEncounteredNotifier({
required this.ref,
required this.featureId,
}) : super(const AsyncData(99));
final String featureId;
final Ref ref;
Future<void> fix(int amount) async {
state = await AsyncValue.guard(() async {
final old = state.requireValue;
final result =
await ref.read(taskTrackerProvider).fix(id: featureId, fixed: amount);
return max(old - result, 0);
});
}
}
final bugsEncounteredNotifierProvider = StateNotifierProvider.family
.autoDispose<BugsEncounteredNotifier, AsyncValue<int>, String>((ref, id) {
return BugsEncounteredNotifier(ref: ref, featureId: id);
});
BugsEncounteredNotifier biraz... ağır / okuması zor hissettiriyor.
Geçişi yapılmış AsyncNotifier karşılığına bir göz atalım:
- riverpod
- riverpod_generator
class BugsEncounteredNotifier extends AsyncNotifier<int> {
BugsEncounteredNotifier(this.arg);
final String arg;
FutureOr<int> build() {
return 99;
}
Future<void> fix(int amount) async {
final old = await future;
final result =
await ref.read(taskTrackerProvider).fix(id: this.arg, fixed: amount);
state = AsyncData(max(old - result, 0));
}
}
final bugsEncounteredNotifierProvider = AsyncNotifierProvider.family
.autoDispose<BugsEncounteredNotifier, int, String>(
BugsEncounteredNotifier.new,
);
class BugsEncounteredNotifier extends _$BugsEncounteredNotifier {
FutureOr<int> build(String featureId) {
return 99;
}
Future<void> fix(int amount) async {
final old = await future;
final result = await ref
.read(taskTrackerProvider)
.fix(id: this.featureId, fixed: amount);
state = AsyncData(max(old - result, 0));
}
}
Geçişi yapılmış karşılığı, okuması hafif bir metin gibi hissettirmeli.
(Async)Notifier'ın .family parametrelerine this.arg üzerinden (kod üretimi kullanırken this.paramName üzerinden) erişilebilir
Yaşam döngülerinin davranışı farklıdır
Notifier/AsyncNotifier ile StateNotifier arasındaki yaşam döngüleri esaslı biçimde farklılık gösterir.
Bu örnek, eski API'nin mantığı nasıl dağınık biçimde barındırdığını -bir kez daha- gösteriyor:
class MyNotifier extends StateNotifier<int> {
MyNotifier(this.ref, this.period) : super(0) {
// 1 init logic
_timer = Timer.periodic(period, (t) => update()); // 2 side effect on init
}
final Duration period;
final Ref ref;
late final Timer _timer;
Future<void> update() async {
await ref.read(repositoryProvider).update(state + 1); // 3 mutation
if (mounted) state++; // 4 check for mounted props
}
void dispose() {
_timer.cancel(); // 5 custom dispose logic
super.dispose();
}
}
final myNotifierProvider = StateNotifierProvider<MyNotifier, int>((ref) {
// 6 provider definition
final period = ref.watch(durationProvider); // 7 reactive dependency logic
return MyNotifier(ref, period); // 8 pipe down `ref`
});
Burada durationProvider güncellenirse MyNotifier yok edilir: örneği yeniden oluşturulur
ve iç durumu yeniden başlatılır.
Dahası, diğer tüm provider'ların aksine dispose geri çağırmasının sınıfın içinde,
ayrıca tanımlanması gerekir.
Son olarak, provider'ının içine ref.onDispose yazmak hâlâ mümkündür; bu da bu API ile mantığın
ne kadar dağınık olabileceğini bir kez daha gösterir; geliştirici, bu Notifier'ın davranışını anlamak
için sekiz (8!) farklı yere bakmak zorunda kalabilir!
Bu belirsizlikler Riverpod 2.0 ile çözülmüştür.
Eski dispose ile ref.onDispose karşılaştırması
StateNotifier'ın dispose metodu, notifier'ın kendisinin yok edilme olayına işaret eder; yani
kendisi yok edilmeden önce çağrılan bir geri çağırmadır.
(Async)Notifier'ların böyle bir özelliği yoktur; çünkü yeniden oluşturmada yok edilmezler, yalnızca
iç durumları yok edilir.
Yeni notifier'larda yok etme yaşam döngüleri, tıpkı diğer provider'larda olduğu gibi yalnızca tek bir
yerde, ref.onDispose (ve benzerleri) üzerinden ele alınır.
Bu, API'yi ve umarız geliştirici deneyimini de sadeleştirir; böylece yaşam döngüsü yan etkilerini
anlamak için bakılacak yalnızca tek bir yer olur: build metodu.
Kısacası: iç durumu yeniden oluşturulmadan önce tetiklenecek bir geri çağırma kaydetmek için,
diğer tüm provider'larda olduğu gibi ref.onDispose kullanabiliriz.
Yukarıdaki kod parçasını şu şekilde geçirebilirsiniz:
- riverpod
- riverpod_generator
class MyNotifier extends Notifier<int> {
int build() {
// Kodu tek bir yerde, burada okuyup yazmanız yeterli
final period = ref.watch(durationProvider);
final timer = Timer.periodic(period, (t) => update());
ref.onDispose(timer.cancel);
return 0;
}
Future<void> update() async {
await ref.read(repositoryProvider).update(state + 1);
// `mounted` artık yok!
state++; // Bu istisna fırlatabilir.
}
}
final myNotifierProvider = NotifierProvider<MyNotifier, int>(MyNotifier.new);
class MyNotifier extends _$MyNotifier {
int build() {
// Kodu tek bir yerde, burada okuyup yazmanız yeterli
final period = ref.watch(durationProvider);
final timer = Timer.periodic(period, (t) => update());
ref.onDispose(timer.cancel);
return 0;
}
Future<void> update() async {
await ref.read(repositoryProvider).update(state + 1);
// `mounted` artık yok!
state++; // Bu istisna fırlatabilir.
}
}
Bu son kod parçasında elbette bir miktar sadeleşme var, ancak hâlâ açık bir sorun var: update
gerçekleştirilirken notifier'larımızın hâlâ canlı olup olmadığını artık anlayamıyoruz.
Bu da istenmeyen StateError'lara yol açabilir.
Artık mounted yok
Bunun nedeni, (Async)Notifier'larda StateNotifier'da bulunan mounted özelliğinin
olmamasıdır.
Yaşam döngülerindeki farkı düşününce bu gayet mantıklı; mümkün olsa bile bir mounted
özelliği yeni notifier'larda yanıltıcı olurdu: mounted neredeyse her zaman true olurdu.
Özel bir geçici çözüm üretmek mümkün olsa da, bunun yerine asenkron işlemi iptal ederek çözüm bulmanız önerilir.
Bir işlemi iptal etmek, özel bir Completer veya ondan türetilmiş herhangi bir özel yapıyla yapılabilir.
Örneğin ağ istekleri için Dio kullanıyorsanız bir cancel token
kullanmayı düşünün (ayrıca bkz. Otomatik yok etme).
Dolayısıyla yukarıdaki örnek şuna dönüşür:
- riverpod
- riverpod_generator
class MyNotifier extends Notifier<int> {
int build() {
// Kodu tek bir yerde, burada okuyup yazmanız yeterli
final period = ref.watch(durationProvider);
final timer = Timer.periodic(period, (t) => update());
ref.onDispose(timer.cancel);
return 0;
}
Future<void> update() async {
final cancelToken = CancelToken();
ref.onDispose(cancelToken.cancel);
await ref.read(repositoryProvider).update(state + 1, token: cancelToken);
state++;
}
}
final myNotifierProvider = NotifierProvider<MyNotifier, int>(MyNotifier.new);
class MyNotifier extends _$MyNotifier {
int build() {
// Kodu tek bir yerde, burada okuyup yazmanız yeterli
final period = ref.watch(durationProvider);
final timer = Timer.periodic(period, (t) => update());
ref.onDispose(timer.cancel);
return 0;
}
Future<void> update() async {
final cancelToken = CancelToken();
ref.onDispose(cancelToken.cancel);
await ref.read(repositoryProvider).update(state + 1, token: cancelToken);
// `cancelToken.cancel` çağrıldığında özel bir Exception fırlatılır
state++;
}
}
Mutation API'leri öncekiyle aynıdır
Şimdiye kadar StateNotifier ile yeni API'ler arasındaki farkları gösterdik.
Buna karşılık Notifier, AsyncNotifier ve StateNotifier'ın paylaştığı bir şey var: durumlarının
nasıl tüketilip değiştirilebileceği.
Consumer'lar bu üç provider'dan da aynı sözdizimiyle veri elde edebilir; bu da StateNotifier'dan
geçiş yapıyorsanız harika bir şey. Aynısı notifier metotları için de geçerlidir.
class SomeConsumer extends ConsumerWidget {
const SomeConsumer({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final counter = ref.watch(counterNotifierProvider);
return Column(
children: [
Text("You've counted up until $counter, good job!"),
TextButton(
onPressed: () =>
ref.read(counterNotifierProvider.notifier).increment(),
child: const Text('Count even more!'),
),
],
);
}
}
Diğer geçişler
StateNotifier ile Notifier (veya AsyncNotifier) arasındaki daha az etkili farkları inceleyelim
.addListener ve .stream'den geçiş
StateNotifier'ın .addListener ve .stream metotları durum değişikliklerini dinlemek için kullanılabilir.
Bu iki API artık geçmişte kalmış sayılmalıdır.
Bu, Notifier, AsyncNotifier ve diğer provider'larla tam API tutarlılığına ulaşma isteği nedeniyle bilinçli bir tercihtir.
Gerçekten de bir Notifier veya AsyncNotifier kullanmak, diğer herhangi bir provider'dan farklı olmamalıdır.
Dolayısıyla şu:
class MyNotifier extends StateNotifier<int> {
MyNotifier() : super(0);
void add() => state++;
}
final myNotifierProvider = StateNotifierProvider<MyNotifier, int>((ref) {
final notifier = MyNotifier();
final cleanup = notifier.addListener((state) => debugPrint('$state'));
ref.onDispose(cleanup);
// Ya da eşdeğer olarak:
// final listener = notifier.stream.listen((event) => debugPrint('$event'));
// ref.onDispose(listener.cancel);
return notifier;
});
Şuna dönüşür:
- riverpod
- riverpod_generator
class MyNotifier extends Notifier<int> {
int build() {
listenSelf((_, next) => debugPrint('$next'));
return 0;
}
void add() => state++;
}
final myNotifierProvider = NotifierProvider<MyNotifier, int>(MyNotifier.new);
class MyNotifier extends _$MyNotifier {
int build() {
listenSelf((_, next) => debugPrint('$next'));
return 0;
}
void add() => state++;
}
Özetle: bir Notifier/AsyncNotifier'ı dinlemek istiyorsanız, yalnızca ref.listen kullanın.
Bkz. Ref'ler.
Testlerde .debugState'ten geçiş
StateNotifier, .debugState'i sunar: bu özellik, pkg:state_notifier kullanıcılarının geliştirme
modundayken test amacıyla sınıf dışından duruma erişebilmesi için kullanılır.
Testlerde duruma erişmek için .debugState kullanıyorsanız, büyük olasılıkla bu yaklaşımı
bırakmanız gerekiyor.
Notifier / AsyncNotifier'ın bir .debugState'i yoktur; bunun yerine doğrudan @visibleForTesting
olan .state'i sunarlar.
Testlerden .state'e erişmekten KAÇININ; mecbursanız, bunu yalnızca ve yalnızca zaten düzgün biçimde
örneklenmiş bir Notifier / AsyncNotifier'ınız varsa yapın;
o zaman testlerin içinde .state'e serbestçe erişebilirsiniz.
Gerçekten de Notifier / AsyncNotifier elle örneklenmemelidir; bunun yerine kendi provider'ları
üzerinden etkileşime geçilmelidir: bunu yapmamak, ref ve family argümanları başlatılmadığı için
notifier'ı bozar.
Elinizde bir Notifier örneği yok mu?
Sorun değil, tıpkı sunduğu durumu okuyacağınız gibi ref.read ile bir tane elde edebilirsiniz:
void main(List<String> args) {
test('my test', () {
final container = ProviderContainer();
addTearDown(container.dispose);
// Bir notifier elde etmek
final Notifier<int> notifier = container.read(myNotifierProvider.notifier);
// Sunduğu durumu elde etmek
final int state = container.read(myNotifierProvider);
// TODO testlerinizi yazın
});
}
Test etme hakkında daha fazlasını kendi rehberinde öğrenin. Bkz. Provider'larınızı test etme.
StateProvider'dan geçiş
StateProvider, Riverpod tarafından yayınlandığı günden beri sunuluyordu ve StateNotifierProvider'ın
sadeleştirilmiş sürümlerinde birkaç satır koddan tasarruf etmek için yapılmıştı.
StateNotifierProvider kullanımdan kaldırıldığı için StateProvider'dan da kaçınılmalıdır.
Dahası, şu an itibarıyla yeni API'lerde StateProvider eşdeğeri bulunmuyor.
Yine de StateProvider'dan Notifier'a geçiş basittir.
Şu:
final counterProvider = StateProvider<int>((ref) {
return 0;
});
Şuna dönüşür:
- riverpod
- riverpod_generator
class CounterNotifier extends Notifier<int> {
int build() => 0;
set state(int newState) => super.state = newState;
int update(int Function(int state) cb) => state = cb(state);
}
final counterNotifierProvider =
NotifierProvider<CounterNotifier, int>(CounterNotifier.new);
class CounterNotifier extends _$CounterNotifier {
int build() => 0;
set state(int newState) => super.state = newState;
int update(int Function(int state) cb) => state = cb(state);
}
Bize birkaç satır koda mal olsa da, StateProvider'dan uzaklaşmak StateNotifier'ı
arşivlememizi sağlıyor.