Pull-to-refresh (aşağı çekerek yenileme) uygulama
Riverpod, bildirimsel yapısı sayesinde pull-to-refresh'i doğrudan destekler.
Genel olarak pull-to-refresh karmaşık olabilir, çünkü çözülmesi gereken birden fazla sorun vardır:
- Bir sayfaya ilk girildiğinde bir spinner göstermek isteriz. Ancak yenileme sırasında bunun yerine yenileme göstergesini göstermek isteriz. Yenileme göstergesi ve spinner'ı aynı anda göstermemeliyiz.
- Bir yenileme beklemedeyken önceki veriyi/hatayı göstermek isteriz.
- Yenileme devam ettiği sürece yenileme göstergesini göstermemiz gerekir.
Bunu Riverpod ile nasıl çözeceğimize bakalım.
Bunun için kullanıcılara rastgele bir aktivite öneren basit bir örnek yapacağız.
Ve aşağı çekerek yenileme yapmak yeni bir öneri getirecek:

Temel bir uygulama oluşturma
Pull-to-refresh'i uygulamadan önce, öncelikle yenileyecek bir şeye ihtiyacımız var.
Kullanıcılara rastgele bir aktivite önermek için Bored API
kullanan basit bir uygulama yapabiliriz.
Önce bir Activity sınıfı tanımlayalım:
- riverpod
- riverpod_generator
class Activity {
Activity({
required this.activity,
required this.type,
required this.participants,
required this.price,
});
factory Activity.fromJson(Map<Object?, Object?> json) {
return Activity(
activity: json['activity']! as String,
type: json['type']! as String,
participants: json['participants']! as int,
price: json['price']! as double,
);
}
final String activity;
final String type;
final int participants;
final double price;
}
sealed class Activity with _$Activity {
factory Activity({
required String activity,
required String type,
required int participants,
required double price,
}) = _Activity;
factory Activity.fromJson(Map<String, dynamic> json) =>
_$ActivityFromJson(json);
}
Bu sınıf, önerilen bir aktiviteyi tip güvenli bir şekilde temsil etmekten ve
JSON kodlama/çözme işlemlerini yönetmekten sorumlu olacak.
Freezed/json_serializable kullanmak zorunlu değildir ama önerilir.
Şimdi tek bir aktivite getirmek için HTTP GET isteği yapan bir provider tanımlayalım:
- riverpod
- riverpod_generator
final activityProvider = FutureProvider.autoDispose<Activity>((ref) async {
final response = await http.get(
Uri.https('www.boredapi.com', '/api/activity'),
);
final json = jsonDecode(response.body) as Map;
return Activity.fromJson(json);
});
Future<Activity> activity(Ref ref) async {
final response = await http.get(
Uri.https('www.boredapi.com', '/api/activity'),
);
final json = jsonDecode(response.body) as Map;
return Activity.fromJson(Map.from(json));
}
Artık bu provider'ı rastgele bir aktivite göstermek için kullanabiliriz.
Şimdilik yükleme/hata durumunu yönetmeyeceğiz ve aktiviteyi yalnızca hazır
olduğunda göstereceğiz:
class ActivityView extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final activity = ref.watch(activityProvider);
return Scaffold(
appBar: AppBar(title: const Text('Pull to refresh')),
body: Center(
// Bir activity varsa gösterin, yoksa bekleyin
child: Text(activity.value?.activity ?? ''),
),
);
}
}
RefreshIndicator ekleme
Elimizde basit bir uygulama olduğuna göre, artık ona bir RefreshIndicator
ekleyebiliriz.
Bu widget, kullanıcı ekranı aşağı çektiğinde yenileme göstergesini görüntülemekten
sorumlu resmi bir Material widget'ıdır.
RefreshIndicator kullanmak kaydırılabilir bir yüzey gerektirir. Ancak şu ana
kadar böyle bir şeyimiz yok. Bunu bir ListView/GridView/SingleChildScrollView/vb.
kullanarak çözebiliriz:
class ActivityView extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final activity = ref.watch(activityProvider);
return Scaffold(
appBar: AppBar(title: const Text('Pull to refresh')),
body: RefreshIndicator(
onRefresh: () async => print('refresh'),
child: ListView(
children: [
Text(activity.value?.activity ?? ''),
],
),
),
);
}
}
Kullanıcılar artık ekranı aşağı çekebilir. Ama verimiz henüz yenilenmiyor.
Yenileme mantığını ekleme
Kullanıcılar ekranı aşağı çektiğinde RefreshIndicator, onRefresh geri
çağırmasını çalıştırır. Verimizi yenilemek için bu geri çağırmayı kullanabiliriz.
Orada, seçtiğimiz provider'ı yenilemek için ref.refresh kullanabiliriz.
Not: onRefreshin bir Future döndürmesi beklenir.
Ve bu future'ın yenileme tamamlandığında sonuçlanması önemlidir.
Böyle bir future elde etmek için provider'ımızın .future özelliğini
okuyabiliriz. Bu, provider'ımız çözümlendiğinde sonuçlanan bir future döndürür.
Bu nedenle RefreshIndicatorımızı şu şekilde güncelleyebiliriz:
class ActivityView extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final activity = ref.watch(activityProvider);
return Scaffold(
appBar: AppBar(title: const Text('Pull to refresh')),
body: RefreshIndicator(
// "activityProvider.future" yenilenip sonucu geri döndürüldüğü için,
// yenileme göstergesi yeni activity getirilene kadar görünmeye
// devam eder.
onRefresh: () => ref.refresh(activityProvider.future),
child: ListView(
children: [
Text(activity.value?.activity ?? ''),
],
),
),
);
}
}
Spinner'ı yalnızca ilk yükleme sırasında gösterme ve hataları yönetme
Şu anda kullanıcı arayüzümüz hata/yükleme durumlarını yönetmiyor.
Bunun yerine, yükleme/yenileme bittiğinde veri sihirli bir şekilde beliriveriyor.
Bu durumları düzgünce yöneterek bunu değiştirelim. İki durum söz konusu:
- İlk yükleme sırasında tam ekran bir spinner göstermek istiyoruz.
- Yenileme sırasında yenileme göstergesini ve önceki veriyi/hatayı göstermek istiyoruz.
Neyse ki Riverpod'da asenkron bir provider dinlerken Riverpod bize ihtiyacımız
olan her şeyi sunan bir AsyncValue verir.
Bu AsyncValue, Dart 3.0'ın desen eşleme (pattern matching) özelliğiyle şu
şekilde birleştirilebilir:
class ActivityView extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final activity = ref.watch(activityProvider);
return Scaffold(
appBar: AppBar(title: const Text('Pull to refresh')),
body: RefreshIndicator(
onRefresh: () => ref.refresh(activityProvider.future),
child: ListView(
children: [
switch (activity) {
// Veri mevcutsa onu gösteriyoruz.
// Yenileme sırasında da verinin hâlâ mevcut olacağını unutmayın.
AsyncValue<Activity>(:final value?) => Text(value.activity),
// Bir hata mevcut, dolayısıyla onu gösteriyoruz.
AsyncValue(:final error?) => Text('Error: $error'),
// Veri/hata yok, yani yükleme durumundayız.
_ => const CircularProgressIndicator(),
},
],
),
),
);
}
}
Burada valueOrNull kullanıyoruz, çünkü şu anda value kullanmak hata/yükleme
durumundayken istisna fırlatıyor.
Riverpod 3.0 bunu değiştirecek ve value, valueOrNull gibi davranacak.
Ama şimdilik valueOrNullda kalalım.
Desen eşlememizde :final valueOrNull? sözdiziminin kullanımına dikkat edin.
Bu sözdizimi yalnızca activityProvider null olamayan bir Activity
döndürdüğü için kullanılabiliyor.
Verileriniz null olabiliyorsa bunun yerine
AsyncValue(hasData: true, :final valueOrNull) kullanabilirsiniz.
Bu, birkaç fazladan karakter pahasına verinin null olduğu durumları da doğru
şekilde yönetir.
Toparlayalım: uygulamanın tamamı
İşte şimdiye kadar ele aldığımız her şeyin birleştirilmiş kaynak kodu:
- riverpod
- riverpod_generator
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
void main() => runApp(ProviderScope(child: MyApp()));
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(home: ActivityView());
}
}
class ActivityView extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final activity = ref.watch(activityProvider);
return Scaffold(
appBar: AppBar(title: const Text('Pull to refresh')),
body: RefreshIndicator(
onRefresh: () => ref.refresh(activityProvider.future),
child: ListView(
children: [
switch (activity) {
AsyncValue<Activity>(:final value?) => Text(value.activity),
AsyncValue(:final error?) => Text('Error: $error'),
_ => const CircularProgressIndicator(),
},
],
),
),
);
}
}
final activityProvider = FutureProvider.autoDispose<Activity>((ref) async {
final response = await http.get(
Uri.https('www.boredapi.com', '/api/activity'),
);
final json = jsonDecode(response.body) as Map;
return Activity.fromJson(json);
});
class Activity {
Activity({
required this.activity,
required this.type,
required this.participants,
required this.price,
});
factory Activity.fromJson(Map<Object?, Object?> json) {
return Activity(
activity: json['activity']! as String,
type: json['type']! as String,
participants: json['participants']! as int,
price: json['price']! as double,
);
}
final String activity;
final String type;
final int participants;
final double price;
}
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:http/http.dart' as http;
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'codegen.g.dart';
part 'codegen.freezed.dart';
void main() => runApp(ProviderScope(child: MyApp()));
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(home: ActivityView());
}
}
class ActivityView extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final activity = ref.watch(activityProvider);
return Scaffold(
appBar: AppBar(title: const Text('Pull to refresh')),
body: RefreshIndicator(
onRefresh: () => ref.refresh(activityProvider.future),
child: ListView(
children: [
switch (activity) {
AsyncValue<Activity>(:final value?) => Text(value.activity),
AsyncValue(:final error?) => Text('Error: $error'),
_ => const CircularProgressIndicator(),
},
],
),
),
);
}
}
Future<Activity> activity(Ref ref) async {
final response = await http.get(
Uri.https('www.boredapi.com', '/api/activity'),
);
final json = jsonDecode(response.body) as Map;
return Activity.fromJson(Map.from(json));
}
sealed class Activity with _$Activity {
factory Activity({
required String activity,
required String type,
required int participants,
required double price,
}) = _Activity;
factory Activity.fromJson(Map<String, dynamic> json) =>
_$ActivityFromJson(json);
}