Ana içeriğe atla

`ChangeNotifier`'dan geçiş

Riverpod içinde ChangeNotifierProvider, pkg:provider'dan yumuşak bir geçiş sunmak amacıyla kullanılmak üzere tasarlanmıştır.

pkg:riverpod'a geçişe henüz başladıysanız, kendisine ayrılmış rehberi okuduğunuzdan emin olun (bkz. Hızlı başlangıç). Bu yazı, riverpod'a çoktan geçmiş ama ChangeNotifier'dan uzaklaşmak isteyen kişiler içindir.

Sonuç olarak ChangeNotifier'dan AsyncNotifier'a geçiş bir paradigma değişimi gerektirir, ancak geçişi yapılmış kodda büyük bir sadeleşme getirir.

Şu (kusurlu) örneği ele alalım:

class MyChangeNotifier extends ChangeNotifier {
MyChangeNotifier() {
_init();
}
List<Todo> todos = [];
bool isLoading = true;
bool hasError = false;

Future<void> _init() async {
try {
final json = await http.get('api/todos');
todos = [...json.map(Todo.fromJson)];
} on Exception {
hasError = true;
} finally {
isLoading = false;
notifyListeners();
}
}

Future<void> addTodo(int id) async {
isLoading = true;
notifyListeners();

try {
final json = await http.post('api/todos');
todos = [...json.map(Todo.fromJson)];
hasError = false;
} on Exception {
hasError = true;
} finally {
isLoading = false;
notifyListeners();
}
}
}

final myChangeProvider = ChangeNotifierProvider<MyChangeNotifier>((ref) {
return MyChangeNotifier();
});

Bu uygulama, şunlar gibi birkaç zayıf tasarım tercihini gözler önüne seriyor:

  • Farklı asenkron durumları ele almak için isLoading ve hasError kullanımı
  • İstekleri yorucu try/catch/finally ifadeleriyle dikkatle ele alma gerekliliği
  • Bu uygulamanın çalışması için notifyListeners'ın doğru anlarda çağrılması gerekliliği
  • Tutarsız veya muhtemelen istenmeyen durumların varlığı, örneğin boş bir listeyle başlatma

Bu örneğin, ChangeNotifier'ın yeni başlayan geliştiricileri nasıl kusurlu tasarım tercihlerine sürükleyebileceğini göstermek için özellikle kurgulandığına dikkat edin; ayrıca bir başka çıkarım da, değiştirilebilir durumun başta vaat ettiğinden çok daha zor olabileceğidir.

Notifier/AsyncNotifier, değişmez (immutable) durumla birleştiğinde daha iyi tasarım tercihlerine ve daha az hataya yol açabilir.

Yukarıdaki kod parçasını adım adım en yeni API'lere nasıl geçireceğimize bakalım.

Geçişinize başlayın

Öncelikle yeni provider'ı / notifier'ı tanımlamalıyız: bu, kendine özgü iş mantığınıza bağlı olan bir düşünme sürecini gerektirir.

Yukarıdaki gereksinimleri özetleyelim:

  • Durum, parametresiz bir ağ çağrısıyla elde edilen List<Todo> ile temsil ediliyor
  • Durum, ayrıca loading, error ve data durumları hakkında bilgi sunmalı
  • Durum, dışa açılan bazı metotlarla değiştirilebilmeli; dolayısıyla bir fonksiyon yeterli değil
ipucu

Yukarıdaki düşünme süreci şu soruları yanıtlamaya indirgenir:

  1. Bazı yan etkiler gerekiyor mu?
    • y: riverpod'un sınıf tabanlı API'sini kullanın
    • n: riverpod'un fonksiyon tabanlı API'sini kullanın
  2. Durumun asenkron olarak yüklenmesi gerekiyor mu?
    • y: build bir Future<T> döndürsün
    • n: build doğrudan T döndürsün
  3. Bazı parametreler gerekiyor mu?
    • y: build (veya fonksiyonunuz) bunları kabul etsin
    • n: build (veya fonksiyonunuz) fazladan parametre kabul etmesin
bilgi

Kod üretimi kullanıyorsanız yukarıdaki düşünme süreci yeterlidir.
Doğru sınıf adlarını ve onların özel API'lerini düşünmenize gerek yoktur.
@riverpod sizden yalnızca dönüş tipiyle birlikte bir sınıf yazmanızı ister, gerisi hallolur.

Teknik olarak buradaki en uygun seçim, yukarıdaki tüm gereksinimleri karşılayan bir AsyncNotifier<List<Todo>> tanımlamaktır. Önce biraz sözde kod yazalım.

class MyNotifier extends AsyncNotifier<List<Todo>> {

FutureOr<List<Todo>> build() {
// TODO ...
return [];
}

Future<void> addTodo(Todo todo) async {
// TODO
}
}

final myNotifierProvider =
AsyncNotifierProvider.autoDispose<MyNotifier, List<Todo>>(MyNotifier.new);
ipucu

Unutmayın: yol göstermesi için ya da yalnızca kod yazmanızı hızlandırmak için IDE'nizdeki snippet'leri kullanın. Bkz. Başlarken.

ChangeNotifier'ın uygulamasına kıyasla artık todosu tanımlamamıza gerek yok; bu değişkenin karşılığı, build ile örtük olarak yüklenen statetir.

Gerçekten de riverpod'un notifier'ları tek seferde bir varlık sunabilir.

ipucu

Riverpod'un API'si ince taneli olacak şekilde tasarlanmıştır; yine de geçiş sırasında birden fazla değeri tutmak için özel bir varlık tanımlayabilirsiniz. Başlangıçta geçişi yumuşatmak için Dart 3'ün record'larını kullanmayı düşünün.

Başlatma

Bir notifier'ı başlatmak kolaydır: başlatma mantığını build içine yazmanız yeterli. Artık eski _init fonksiyonundan kurtulabiliriz.

class MyNotifier extends AsyncNotifier<List<Todo>> {

FutureOr<List<Todo>> build() async {
final json = await http.get('api/todos');
return [...json.map(Todo.fromJson)];
}
}

final myNotifierProvider =
AsyncNotifierProvider.autoDispose<MyNotifier, List<Todo>>(MyNotifier.new);

Eski _inite kıyasla yeni build hiçbir şeyi eksik bırakmıyor: artık isLoading veya hasError gibi değişkenleri başlatmaya gerek yok.

Riverpod, bir AsyncValue<List<Todo>> sunarak her asenkron provider'ı otomatik olarak çevirir ve asenkron durumun inceliklerini iki basit boolean bayrağın yapabileceğinden çok daha iyi ele alır.

Gerçekten de her AsyncNotifier, asenkron durumu ele almak için ek try/catch/finally yazmayı fiilen bir anti-desen hâline getirir.

Mutation'lar ve yan etkiler

Tıpkı başlatmada olduğu gibi, yan etkiler gerçekleştirirken de hasError gibi boolean bayrakları yönetmeye ya da ek try/catch/finally blokları yazmaya gerek yoktur.

Aşağıda tüm standart şablon kodu kaldırdık ve yukarıdaki örneğin geçişini başarıyla tamamladık:

class MyNotifier extends AsyncNotifier<List<Todo>> {

FutureOr<List<Todo>> build() async {
final json = await http.get('api/todos');

return [...json.map(Todo.fromJson)];
}

Future<void> addTodo(Todo todo) async {
// optional: state = const AsyncLoading();
final json = await http.post('api/todos');
final newTodos = [...json.map(Todo.fromJson)];
state = AsyncData(newTodos);
}
}

final myNotifierProvider =
AsyncNotifierProvider.autoDispose<MyNotifier, List<Todo>>(MyNotifier.new);
ipucu

Sözdizimi ve tasarım tercihleri değişebilir, ancak sonuçta tek yapmamız gereken isteğimizi yazmak ve ardından durumu güncellemektir. Bkz. Provider'lar.

Geçiş sürecinin özeti

Yukarıda uyguladığımız geçiş sürecinin tamamını işlemsel bir bakış açısıyla gözden geçirelim.

  1. Başlatmayı, yapıcıda çağrılan özel bir metottan alıp build içine taşıdık
  2. todos, isLoading ve hasError özelliklerini kaldırdık: içerideki state yeterli olacak
  3. Tüm try-catch-finally bloklarını kaldırdık: future'ı döndürmek yeterli
  4. Aynı sadeleştirmeyi yan etkilere de (addTodo) uyguladık
  5. Mutation'ları, yalnızca statei yeniden atayarak uyguladık