Sparround

Repository implementasiyası: həqiqətin mənbəyi

Repository data qatının qərar verən hissəsidir. Rəsmi offline-first sənədi onun rolunu birbaşa formalaşdırır: repository-lər vahid həqiqət mənbəyidir və məlumatın dəyişdirilə biləcəyi yeganə yer olmalıdır.

Repository-nin işləri:

  • Bir və ya bir neçə service-i çağırmaq.
  • DTO-nu domain modelinə çevirmək (mapper vasitəsilə).
  • Cache saxlamaq və onun ömrünə qərar vermək.
  • Xətanı domain dilinə çevirmək (Failure / Result).
  • Lazım gəldikdə təkrar cəhd, paralel sorğuların birləşdirilməsi, canlı axın (Stream).

Diqqət yetirilməli nüans: repository-lər bir-birini tanımır. İki repository-nin məlumatı lazımdırsa, birləşdirmə view model-də ya da use-case-də aparılır. Bu qayda dairəvi asılılığın qarşısını alır: OrderRepositoryUserRepositoryOrderRepository kimi zəncirlər testi və başlatmanı mümkünsüz edir.

Sessiya kimi tətbiq boyu paylaşılan state də repository-də yaşayır — çünki o, məlumat üçün vahid mənbədir.

dart
class ProductRepositoryRemote implements ProductRepository {
  ProductRepositoryRemote({
    required ProductApiClient apiClient,
    Duration cacheTtl = const Duration(minutes: 5),
  })  : _apiClient = apiClient,
        _cacheTtl = cacheTtl;

  final ProductApiClient _apiClient;
  final Duration _cacheTtl;

  // Cache: məlumat + vaxt möhürü. Bu, repository-nin state-idir.
  List<Product>? _cache;
  DateTime? _cachedAt;

  // Paralel sorğuların birləşdirilməsi: eyni anda 3 ekran açılsa,
  // şəbəkəyə BİR sorğu gedir.
  Future<List<Product>>? _inFlight;

  final _controller = StreamController<List<Product>>.broadcast();

  bool get _isCacheFresh =>
      _cache != null &&
      _cachedAt != null &&
      DateTime.now().difference(_cachedAt!) < _cacheTtl;

  @override
  Future<List<Product>> fetchActive({bool forceRefresh = false}) {
    if (!forceRefresh && _isCacheFresh) return Future.value(_cache);
    return _inFlight ??= _load().whenComplete(() => _inFlight = null);
  }

  Future<List<Product>> _load() async {
    try {
      final dtos = await _apiClient.getActiveProducts();
      final products = dtos.map((dto) => dto.toDomain()).toList();

      _cache = products;
      _cachedAt = DateTime.now();
      _controller.add(products);          // dinləyiciləri xəbərdar edir
      return products;
    } on UnauthorizedException {
      // Texniki exception → domain dilinə çevrilir.
      throw const AuthFailure();
    } on SocketException {
      // Şəbəkə yoxdur: köhnə cache varsa, onu vermək daha yaxşıdır.
      final stale = _cache;
      if (stale != null) return stale;
      throw const NetworkFailure();
    }
  }

  @override
  Stream<List<Product>> watchActive() async* {
    if (_cache != null) yield _cache!;    // dərhal cache
    yield* _controller.stream;            // sonra yeniləmələr
  }

  @override
  Future<Product> fetchById(String id) async {
    // Cache-də varsa, şəbəkəyə çıxmır.
    final cached = _cache?.where((p) => p.id == id).firstOrNull;
    if (cached != null) return cached;
    return (await _apiClient.getProduct(id)).toDomain();
  }

  void dispose() => _controller.close();
}

Cache, paralel sorğuların birləşdirilməsi və canlı axın olan repository. Bütün bu qərarlar bir sinifdə cəmlənir — UI heç birini bilmir.

QərarRepository-də niyəUI-da olsa nə olur
Cache-in ömrü (TTL)Bütün ekranlar üçün bir qərarHər ekran öz TTL-ini seçir, məlumat ekranlar arası fərqlənir
Şəbəkə yoxdursa köhnə cache-i verməkBir yerdə: hər ekran avtomatik faydalanırBəzi ekranlar offline işləyir, bəziləri boş qalır
Paralel sorğuların birləşdirilməsiŞəbəkəyə bir sorğu gedirÜç ekran açılanda üç eyni sorğu
Exception → `Failure` çevrilməsiDomain lüğəti qorunurHər widget `SocketException` tutmalı olur
Sessiya (cari istifadəçi)Vahid mənbə; çıxış bir yerdə baş verirİki ekran fərqli istifadəçi göstərə bilir

Cache-in yeri barədə qərar. Yaddaşda saxlanan cache (yuxarıdaki nümunə) tətbiq bağlananda itir. Bu, çox hal üçün kifayətdir və ən sadə variantdır.

Davamlı cache lazımdırsa, rəsmi sənədlər iki dizayn pattern-i verir: açar-dəyər saxlama (kiçik məlumat: ayarlar, son axtarış) və SQL saxlama (siyahılar, əlaqəli məlumat). Vacib nüans: bu saxlama mexanizmi service kimi yazılır (PreferencesService, DatabaseService), repository isə hansı service-dən oxuyacağına qərar verir.

Kimin dispose etməsi. StreamController istifadə edən repository-nin dispose metodu olmalıdır. Riverpod-da bu, provider-in içində bağlanır:

ref.onDispose(repository.dispose) — provider söküldükdə controller da bağlanır.

Nə vaxt cache yazmamaq. Cache həmişə faydalı deyil: tez-tez dəyişən məlumat (canlı qiymətlər, bildirişlərin sayı) üçün köhnə dəyər göstərmək istifadəçini çaşdırır. Belə hallarda Stream və qısa TTL, ya da ümumiyyətlə cache-siz oxuma daha düzgündür.

Praktika. Layihəndəki bir repository-yə üç şey əlavə et: (1) TTL-li yaddaş cache-i, (2) paralel sorğuların birləşdirilməsi (_inFlight), (3) şəbəkə xətasında köhnə cache-ə fallback. Sonra üç test yaz: cache təzə olduqda service çağırılmır; iki paralel çağırış bir sorğu yaradır; şəbəkə xətasında köhnə məlumat qaytarılır.

Hazır sayılır: üç test də yaşıldır və testlərdə fake service çağırış sayını sayır (callCount).

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

  • Offline-first dizayn pattern-irəsmidocs.flutter.dev

    Repository-nin vahid həqiqət mənbəyi olması və oxuma/yazma strategiyaları — bu mövzunun əsas mənbəyi.

  • Case study: data qatırəsmidocs.flutter.dev

    Repository-nin service-lərdən istifadəsi və DTO → domain çevrilməsi real kodda.

  • Açar-dəyər saxlama pattern-irəsmidocs.flutter.dev

    Kiçik məlumatın davamlı saxlanması — service kimi yazılır, repository ondan istifadə edir.

  • SQL saxlama pattern-irəsmidocs.flutter.dev

    Siyahı və əlaqəli məlumatın lokal saxlanması üçün rəsmi yanaşma.