`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
isLoadingvehasErrorkullanımı - İstekleri yorucu
try/catch/finallyifadeleriyle 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,errorvedatadurumları hakkında bilgi sunmalı - Durum, dışa açılan bazı metotlarla değiştirilebilmeli; dolayısıyla bir fonksiyon yeterli değil
Yukarıdaki düşünme süreci şu soruları yanıtlamaya indirgenir:
- Bazı yan etkiler gerekiyor mu?
y: riverpod'un sınıf tabanlı API'sini kullanınn: riverpod'un fonksiyon tabanlı API'sini kullanın
- Durumun asenkron olarak yüklenmesi gerekiyor mu?
y:buildbirFuture<T>döndürsünn:builddoğrudanTdöndürsün
- Bazı parametreler gerekiyor mu?
y:build(veya fonksiyonunuz) bunları kabul etsinn:build(veya fonksiyonunuz) fazladan parametre kabul etmesin
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.
- riverpod
- riverpod_generator
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);
class MyNotifier extends _$MyNotifier {
FutureOr<List<Todo>> build() {
// TODO ...
return [];
}
Future<void> addTodo(Todo todo) async {
// TODO
}
}
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.
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.
- riverpod
- riverpod_generator
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);
class MyNotifier extends _$MyNotifier {
FutureOr<List<Todo>> build() async {
final json = await http.get('api/todos');
return [...json.map(Todo.fromJson)];
}
}
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:
- riverpod
- riverpod_generator
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);
class MyNotifier extends _$MyNotifier {
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);
}
}
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.
- Başlatmayı, yapıcıda çağrılan özel bir metottan alıp
buildiçine taşıdık todos,isLoadingvehasErrorözelliklerini kaldırdık: içeridekistateyeterli olacak- Tüm
try-catch-finallybloklarını kaldırdık: future'ı döndürmek yeterli - Aynı sadeleştirmeyi yan etkilere de (
addTodo) uyguladık - Mutation'ları, yalnızca
statei yeniden atayarak uyguladık