Xətanın ekranda göstərilməsi: AsyncValue, retry, boş hal
Xəta qatlar arasında yol keçdi: service texniki exception atdı, repository onu Failure-a çevirdi, notifier state-ə yerləşdirdi. Son addım — ekranda nə görünəcək.
Bu qatın qərarları texniki deyil, davranış qərarlarıdır:
- Xəta hansı formada göstərilir: tam ekran, banner, snackbar, sahənin altında qırmızı yazı?
- Retry var, yoxsa yox? (
NotFoundFailureüçün təkrar cəhd mənasızdır.) - Köhnə məlumat ekranda qalır, yoxsa silinir?
- Xəta istifadəçinin işini dayandırır, yoxsa yalnız xəbərdarlıqdır?
Bir qayda mütləqdir: `e.toString()` heç vaxt istifadəçiyə göstərilmir. "SocketException: Failed host lookup: 'api.example.com'" mesajı istifadəçiyə heç nə demir, lakin backend-in domenini üzə çıxarır. Mesaj həmişə Failure növündən və lokalizasiyadan yaranır.
İkinci qayda: boş hal xəta deyil. Siyahı boş olduqda "heç nə tapılmadı" göstərilir; bu, AsyncData halının bir variantıdır və xəta ekranı ilə qarışdırılmamalıdır.
| Vəziyyət | Göstərmə forması | Səbəb |
|---|---|---|
| İlk yükləmə uğursuz, məlumat yoxdur | Tam ekran xəta + retry düyməsi | Göstərməli heç nə yoxdur; istifadəçiyə yeganə hərəkət retry-dır |
| Yenilənmə uğursuz, köhnə məlumat var | Snackbar / nazik banner; siyahı yerində qalır | Köhnə məlumatı silmək istifadəçinin işini dayandırar |
| Yazma əməliyyatı uğursuz (submit) | Snackbar ya da form üstündə banner; form doldurulmuş qalır | İstifadəçinin yazdığını itirmək ən pis nəticədir |
| Sahə validasiyası uğursuz | Hər sahənin altında ayrı mesaj | İstifadəçi hansı sahəni düzəltməli olduğunu görməlidir |
| Sessiya bitdi (`AuthFailure`) | Login ekranına yönəltmək + qısa izah | Retry mənasızdır — token yenilənməlidir |
| İcazə yoxdur (`ForbiddenFailure`) | İzahlı ekran; login-ə yönəltməmək | Yenidən giriş vəziyyəti dəyişməyəcək |
| Siyahı boş (xəta yoxdur) | "Heç nə yoxdur" + hərəkət təklifi | Bu, uğurlu nəticədir — xəta ekranı göstərmək yanlışdır |
class OrdersPage extends ConsumerWidget {
const OrdersPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final state = ref.watch(orderListNotifierProvider);
// Yan effekt: sessiya bitdikdə yönəltmə. `ref.listen` `build` içində
// təhlükəsizdir və yalnız DƏYİŞİKLİKdə işləyir (hər rebuild-də deyil).
ref.listen(orderListNotifierProvider, (previous, next) {
if (next case AsyncError(error: AuthFailure())) {
context.go('/login?reason=expired');
}
});
return switch (state) {
// 1) Məlumat var, siyahı boş → BOŞ HAL (xəta deyil)
AsyncData(:final value) when value.orders.isEmpty => EmptyState(
title: l10n.ordersEmptyTitle,
message: l10n.ordersEmptyMessage,
actionLabel: l10n.ordersEmptyAction,
onAction: () => context.go('/catalog'),
),
// 2) Məlumat var → siyahı; yenilənmə xətası varsa nazik banner
AsyncData(:final value) => Column(
children: [
// `hasError` + `hasValue`: yenilənmə uğursuz oldu,
// lakin köhnə məlumat qaldı.
if (state.hasError)
ErrorBanner(
message: l10n.refreshFailed,
onRetry: () => ref
.read(orderListNotifierProvider.notifier)
.refresh(),
),
Expanded(child: OrderListView(orders: value.orders)),
],
),
// 3) Məlumat yoxdur, xəta var → TAM EKRAN
AsyncError(:final error) => _fullScreenError(context, ref, error, l10n),
// 4) İlk yükləmə
_ => const Center(child: CircularProgressIndicator()),
};
}
Widget _fullScreenError(
BuildContext context,
WidgetRef ref,
Object error,
AppLocalizations l10n,
) {
// `Failure` növünə görə mesaj və retry qərarı.
final presentation = error is Failure
? presentFailure(error, l10n)
// Failure deyilsə — bu, gözlənilməyən haldır (baq).
// İstifadəçiyə ümumi mesaj, loqa isə tam məlumat.
: ErrorPresentation(message: l10n.errorGeneric, canRetry: true);
return ErrorView(
message: presentation.message,
onRetry: presentation.canRetry
? () => ref.invalidate(orderListNotifierProvider)
: null, // retry mənasızsa düymə YOXDUR
);
}
}Bütün halların bir yerdə emalı. Diqqət et: `AsyncError` iki cür emal olunur — məlumat varsa banner, yoxsa tam ekran.
Retry-ın həqiqətən işləməsi. Retry düyməsi olub işləməyən ekran ən pis variantdır — istifadəçi eyni nəticəni dəfələrlə alır və səbəbini bilmir.
Riverpod-da iki mexanizm var:
ref.invalidate(provider)— provider-i etibarsız edir, növbəti oxunuşdabuildyenidən işləyir.ref.refresh(provider.future)— dərhal yenidən qurur və nəticəniawaitetməyə imkan verir (pull-to-refresh üçün əlverişlidir).
Vacib nüans: retry yalnız səbəb keçici olduqda mənalıdır. NetworkFailure, TimeoutFailure, ServerFailure — bəli. NotFoundFailure, ForbiddenFailure, ValidationFailure — xeyr; onlarda düymə göstərmək istifadəçini aldadır.
Riverpod 3-ün əlavəsi: provider-in inisializasiyası uğursuz olduqda avtomatik təkrar cəhd (eksponensial gecikmə ilə) var. Bu, əl ilə retry-ı əvəz etmir, lakin keçici şəbəkə problemlərində istifadəçi heç nə görməyə də bilər.
Loading göstəricisinin növü. İlk yükləmədə mərkəzdə spinner normaldır. Yenilənmədə isə köhnə məlumatın üzərində nazik göstərici daha yaxşıdır — AsyncValue əvvəlki dəyəri saxladığı üçün bu, təbii şəkildə alınır. Skeleton (shimmer) yalnız ilk yükləmə üçün mənalıdır.
Ən çox buraxılan hal: boş siyahı. Layihələrin çoxunda AsyncData üçün bir budaq olur və boş siyahı ekranda ağ səhifə kimi görünür. İstifadəçi "tətbiq sındı" düşünür.
Hər siyahı ekranı dörd halı emal etməlidir: yüklənir, boş, məlumat, xəta. Beşinci hal — "məlumat var, yenilənmə uğursuz oldu" — bonusdur, lakin əksər ekranda faydalıdır.
Praktika. Bir ekranı bu dörd halın hamısı üçün qur və hamısını öz gözünlə yoxla. Yoxlamağın ən asan yolu — əvvəlki mərhələdə yazdığın ...Local implementasiyası: boş siyahı qaytar, xəta at, gecikmə əlavə et.
Hazır sayılır: dörd hal da real ekranda görünür, retry düyməsi həqiqətən yeniləyir, və heç bir halda e.toString() göstərilmir.
📚 Mənbələr və sənədlər
- Result obyektləri ilə xəta idarəsirəsmidocs.flutter.dev
Xətanın view model-dən view-a necə çatdığı və orada necə emal olunduğu.
- Riverpod: nə yenidir (3.0)rəsmiriverpod.dev
Sealed `AsyncValue`, `valueOrNull` → `value` adı və avtomatik retry mexanizmi.
- Flutter: lokalizasiyarəsmidocs.flutter.dev
Xəta mesajlarının `AppLocalizations` vasitəsilə saxlanması.