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: OrderRepository → UserRepository → OrderRepository 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.
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ərar | Repository-də niyə | UI-da olsa nə olur |
|---|---|---|
| Cache-in ömrü (TTL) | Bütün ekranlar üçün bir qərar | Hər ekran öz TTL-ini seçir, məlumat ekranlar arası fərqlənir |
| Şəbəkə yoxdursa köhnə cache-i vermək | Bir yerdə: hər ekran avtomatik faydalanır | Bə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əsi | Domain lüğəti qorunur | Hə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.