Ana içeriğe atla

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:

Az önce anlatılan uygulamanın çalışırken çekilmiş gif görüntüsü

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:

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

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:

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

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(),
},
],
),
),
);
}
}
DİKKAT

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.

ipucu

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:

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