Sparround

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ətGöstərmə formasıSəbəb
İlk yükləmə uğursuz, məlumat yoxdurTam ekran xəta + retry düyməsiGöstərməli heç nə yoxdur; istifadəçiyə yeganə hərəkət retry-dır
Yenilənmə uğursuz, köhnə məlumat varSnackbar / nazik banner; siyahı yerində qalırKö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ğursuzHə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 izahRetry mənasızdır — token yenilənməlidir
İcazə yoxdur (`ForbiddenFailure`)İzahlı ekran; login-ə yönəltməməkYenidən giriş vəziyyəti dəyişməyəcək
Siyahı boş (xəta yoxdur)"Heç nə yoxdur" + hərəkət təklifiBu, uğurlu nəticədir — xəta ekranı göstərmək yanlışdır
dart
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şda build yenidən işləyir.
  • ref.refresh(provider.future) — dərhal yenidən qurur və nəticəni await etmə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