Sparround

Notifier = view model: Riverpod-da presentation qatı

Rəsmi bələdçidə view model iki iş görür: repository-dən gələn məlumatı ekran state-inə çevirirview rebuild olarkən state-i saxlayır. Riverpod-da bu rolu Notifier / AsyncNotifier oynayır.

Uyğunluq belədir:

  • AsyncNotifier<T> — məlumat yükləyən ekranlar üçün: build metodu Future<T> qaytarır, AsyncValue avtomatik idarə olunur.
  • Notifier<T> — sinxron state üçün: form, filtr, seçim.
  • Command rolunu notifier-in metodları oynayır: refresh(), submit(), toggleFilter(...).

Notifier-in məsuliyyəti:

  • Repository (ya da use-case) çağırmaq.
  • Nəticəni ekran state-inə çevirmək.
  • İstifadəçi hərəkətlərini qəbul etmək (command-lar).
  • Ekrana bağlı state-i saxlamaq: filtr, seçilmiş element, səhifə nömrəsi.

Notifier-in məsuliyyəti olmayan şeylər: HTTP, JSON, cache siyasəti (bunlar data qatındadır), biznes qaydaları (domain modelində ya da use-case-də), naviqasiya və dialoq (bunlar view-un işidir — növbəti mövzu).

dart
// fayl: lib/ui/orders/order_list_notifier.dart
part 'order_list_notifier.g.dart';

@riverpod
class OrderListNotifier extends _$OrderListNotifier {
  static const _pageSize = 20;

  @override
  Future<OrderListState> build() async {
    // İlk yükləmə: repository çağırılır, nəticə ekran state-inə çevrilir.
    final orders = await _repository.fetchMine(pageSize: _pageSize);
    return OrderListState(
      orders: orders,
      filter: OrderFilter.all,
      hasMore: orders.length == _pageSize,
    );
  }

  OrderRepository get _repository => ref.read(orderRepositoryProvider);

  // ── Command: filtr dəyişikliyi (yalnız lokal state) ──
  void setFilter(OrderFilter filter) {
    final current = state.value;
    if (current == null) return;
    state = AsyncValue.data(current.copyWith(filter: filter));
    // DİQQƏT: filtr serverə getmirsə, siyahı yenidən yüklənmir.
    // Süzgəc `build`-də hesablanır — bax: aşağıdaki `visibleOrders`.
  }

  // ── Command: növbəti səhifə ──
  Future<void> loadMore() async {
    final current = state.value;
    if (current == null || !current.hasMore || current.isLoadingMore) return;

    // Köhnə siyahı ekranda qalır, altında spinner görünür.
    state = AsyncValue.data(current.copyWith(isLoadingMore: true));

    final next = await _repository.fetchMine(
      page: current.page + 1,
      pageSize: _pageSize,
    );

    state = AsyncValue.data(current.copyWith(
      orders: [...current.orders, ...next],
      page: current.page + 1,
      hasMore: next.length == _pageSize,
      isLoadingMore: false,
    ));
  }

  // ── Command: yazma əməliyyatı, nəticəni view-a qaytarır ──
  Future<CancelOutcome> cancel(String orderId) async {
    final result = await _repository.cancel(orderId);

    switch (result) {
      case Ok(:final value):
        // Optimistik deyil: server cavabı ilə siyahı yenilənir.
        final current = state.value;
        if (current != null) {
          state = AsyncValue.data(current.copyWith(
            orders: [
              for (final o in current.orders)
                if (o.id == orderId) value else o,
            ],
          ));
        }
        return CancelOutcome.success;
      case Error(error: OrderNotCancellableFailure()):
        return CancelOutcome.notCancellable;
      case Error():
        return CancelOutcome.failed;
    }
  }
}

// ── Törəmə dəyər: süzgəcdən keçmiş siyahı ──
// Ayrı provider kimi yazmaq notifier-i sadə saxlayır və
// yalnız filtr dəyişdikdə yenidən hesablanır.
@riverpod
List<Order> visibleOrders(Ref ref) {
  final state = ref.watch(orderListNotifierProvider).value;
  if (state == null) return const [];
  return switch (state.filter) {
    OrderFilter.all => state.orders,
    OrderFilter.active =>
      state.orders.where((o) => o.status.isActive).toList(),
    OrderFilter.completed =>
      state.orders.where((o) => o.status == OrderStatus.completed).toList(),
  };
}

Notifier-in tam nümunəsi: yükləmə, filtr, səhifələmə və bir yazma əməliyyatı. Diqqət et — heç bir HTTP, JSON ya da `BuildContext`.

Rəsmi bələdçidəRiverpod-daQeyd
View model`Notifier` / `AsyncNotifier`Bir ekrana bir notifier — əlaqə bir-birədir
View model-in state-i`state` sahəsi (`AsyncValue<T>` ya da `T`)Immutable olmalıdır — `copyWith` ilə yenilənir
CommandNotifier-in public metodu`Future<void>` ya da nəticə (`enum`) qaytarır
View model-in asılılıqları`ref.watch(...)` / `ref.read(...)``watch` — dəyişiklikdə yenidən qurulur; `read` — bir dəfəlik
Loading / error state-i`AsyncValue``build`-dən atılan exception avtomatik `AsyncError` olur
Törəmə dəyər (hesablanmış)Ayrı providerNotifier-i sadə saxlayır, yalnız asılılıq dəyişdikdə hesablanır

`ref.watch` yoxsa `ref.read`? Bu, notifier yazarkən ən çox səhv edilən yerdir və qaydası sadədir:

  • `build` içindəref.watch(...): asılılıq dəyişdikdə notifier yenidən qurulur. Repository provider-i dəyişdikdə (məsələn override edildikdə) state avtomatik yenilənir.
  • Metodların içində (command-larda) — ref.read(...): bir dəfəlik çağırış. Command-da watch yazmaq abunəlik yaradır və gözlənilməz təkrar qurulmalara səbəb olur.

Törəmə dəyərləri ayrı provider-də saxlamaq. Süzgəc, sıralama, cəm hesablama kimi işləri notifier-in içində saxlamaq onu şişirdir. Ayrı provider (visibleOrders, cartTotal) iki fayda verir: notifier sadə qalır və hesablama yalnız asılılıq dəyişdikdə aparılır.

Bir ekran, bir notifier. Rəsmi bələdçidə view ilə view model arasındaki əlaqə bir-birədir. Bir notifier-i iki ekranda paylaşmaq istəyirsənsə, deməli paylaşılan hissə əslində repository-yə aiddir — çünki paylaşılan məlumat data qatının işidir.

Praktika. Bir ekranın notifier-ini yaz: build-də repository çağırışı, iki command (biri lokal state dəyişir, biri repository-ni çağırır) və bir törəmə provider (süzgəc ya da cəm).

Sonra notifier üçün test yaz — ProviderContainer.test(overrides: [repositoryProvider.overrideWithValue(Fake...)]) ilə.

Hazır sayılır: notifier faylında http, jsonDecode, BuildContext, Navigator, ScaffoldMessenger yoxdur, və testdə heç bir widget qaldırılmır.

📚 Mənbələr və sənədlər