Ana içeriğe atla

`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 Ref nesnesi 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:

class CounterNotifier extends Notifier<int> {

int build() => 0;

void increment() => state++;
void decrement() => state++;
}

final counterNotifierProvider = NotifierProvider<CounterNotifier, int>(CounterNotifier.new);

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, Notifier bu mantığı build metodunda toplar
  • StateNotifier'ın tüm başlatma süreci provider'ı ile yapıcısı arasında bölünmüşken, Notifier böyle bir mantık için tek bir yer ayırır
  • StateNotifier'ın aksine bir Notifier'ı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:

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

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.

ipucu

StateNotifier<AsyncValue<T>>'dan AsyncNotifier<T>'a geçiş şuna indirgenir:

  • Başlatma mantığını build içine koymak
  • Başlatmadaki veya yan etki metotlarındaki tüm catch/try bloklarını kaldırmak
  • build içindeki tüm AsyncValue.guard kullanı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:

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

Geçişi yapılmış karşılığı, okuması hafif bir metin gibi hissettirmeli.

bilgi

(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:

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);

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:

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);

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:

class MyNotifier extends Notifier<int> {

int build() {
listenSelf((_, next) => debugPrint('$next'));
return 0;
}

void add() => state++;
}

final myNotifierProvider = NotifierProvider<MyNotifier, int>(MyNotifier.new);

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

Uyarı

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:

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);

Bize birkaç satır koda mal olsa da, StateProvider'dan uzaklaşmak StateNotifier'ı arşivlememizi sağlıyor.