Repository müqaviləsi: abstract sinif və asılılığın tərsinə çevrilməsi
Repository müqaviləsi domain qatında elan olunur və yalnız bir sual cavablandırır: tətbiq bu məlumatla nə etmək istəyir? "Necə" sualı — HTTP, cache, baza — implementasiyanın işidir.
Müqavilə yazarkən üç qayda var:
- Yalnız domain tipləri. Giriş və çıxış domain modelləri,
enum-lar, sadə tiplər olur.Response,Map<String, dynamic>,DioException,DocumentSnapshotmüqavilədə görünmür. - Metod adı niyyəti desin.
fetchActive(),watchAll(),save(Order),cancel(String id)— mənbəni deyil, əməliyyatı adlandırır.getFromApi()kimi ad mənbəni sızdırır. - Bir məlumat növü = bir repository. Ekrana görə bölmək (
HomeScreenRepository) səhvdir: ekran dəyişəndə repository dəyişməli olur və iki ekran eyni məlumatı iki dəfə yükləyir.
Dart-da müqavilə abstract class-dır. Dart 3-də abstract interface class də yazmaq olar — bu, sinfin extends edilməsini qadağan edir və yalnız implements icazə verir. Praktik fərq: implementasiya heç bir davranışı miras almır, deməli "yarım implementasiya" mümkün olmur.
// ══ lib/domain/repositories/order_repository.dart ══ (müqavilə)
import 'package:my_app/domain/models/order.dart';
// `abstract interface class`: yalnız `implements` — `extends` qadağandır.
abstract interface class OrderRepository {
/// Cari istifadəçinin sifarişləri, ən yenisi əvvəldə.
Future<List<Order>> fetchMine({int page = 1, int pageSize = 20});
/// Bir sifariş. Tapılmadıqda `OrderNotFoundFailure` qaytarır.
Future<Order> fetchById(String id);
/// Sifarişi ləğv edir və yenilənmiş obyekti qaytarır.
Future<Order> cancel(String id);
/// Siyahının canlı axını — lokal cache dəyişdikdə də yenilənir.
Stream<List<Order>> watchMine();
}
// ══ lib/data/repositories/order_repository_remote.dart ══ (məhsul rejimi)
class OrderRepositoryRemote implements OrderRepository {
OrderRepositoryRemote({required OrderApiClient apiClient})
: _apiClient = apiClient;
final OrderApiClient _apiClient;
final _controller = StreamController<List<Order>>.broadcast();
@override
Future<List<Order>> fetchMine({int page = 1, int pageSize = 20}) async {
final dtos = await _apiClient.getOrders(page: page, limit: pageSize);
final orders = dtos.map((d) => d.toDomain()).toList();
_controller.add(orders); // canlı axını da yeniləyir
return orders;
}
@override
Future<Order> fetchById(String id) async =>
(await _apiClient.getOrder(id)).toDomain();
@override
Future<Order> cancel(String id) async =>
(await _apiClient.cancelOrder(id)).toDomain();
@override
Stream<List<Order>> watchMine() => _controller.stream;
}
// ══ test/fakes/fake_order_repository.dart ══ (test rejimi)
class FakeOrderRepository implements OrderRepository {
FakeOrderRepository(this._orders);
List<Order> _orders;
@override
Future<List<Order>> fetchMine({int page = 1, int pageSize = 20}) async =>
_orders;
@override
Future<Order> fetchById(String id) async =>
_orders.firstWhere((o) => o.id == id);
@override
Future<Order> cancel(String id) async {
final cancelled = (await fetchById(id))
.copyWith(status: OrderStatus.cancelled);
_orders = [
for (final o in _orders) if (o.id == id) cancelled else o,
];
return cancelled;
}
@override
Stream<List<Order>> watchMine() => Stream.value(_orders);
}Bir müqavilə, üç implementasiya. Notifier yalnız müqaviləni tanıyır — hansı implementasiyanın işlədiyini DI qərar verir.
| Müqavilədə OLA BİLƏR | Müqavilədə OLA BİLMƏZ | Niyə |
|---|---|---|
| `Future<List<Order>>` | `Future<Response>` | `Response` HTTP detalıdır — lokal implementasiya onu qaytara bilməz |
| `Order`, `OrderStatus` | `OrderDto`, `Map<String, dynamic>` | DTO backend formasıdır; onu sızdırmaq bütün UI-ı JSON-a bağlayır |
| `String id`, `int page` | `Uri`, `QueryParameters` | Endpoint qurmaq service-in işidir |
| Domain `Failure` tipləri | `DioException`, `SocketException` | Kitabxana exception-ı domain-i həmin kitabxanaya bağlayır |
| `Stream<List<Order>>` | `Stream<QuerySnapshot>` | Baza tipi mənbəni sızdırır və Firestore-dan çıxmağı qeyri-mümkün edir |
`Future`, yoxsa `Stream`? Rəsmi offline-first sənədi bu suala konkret cavab verir: `Stream` daha üstündür, çünki repository iki dəyər verə bilir — əvvəlcə lokal cache-dən sürətli məlumat, sonra serverdən yenilənmiş məlumat. UI hər ikisini avtomatik alır.
Praktikada balans belədir:
- Oxuma (read): cache və ya canlı yeniləmə varsa
Stream; sadə, birdəfəlik sorğudaFuturedaha oxunaqlıdır. - Yazma (write): həmişə
Future— əməliyyat bir dəfə baş verir və nəticəsi (uğur/xəta) birdir.
Riverpod tərəfində hər ikisi rahat işləyir: Future üçün notifier-in build-i Future qaytarır, Stream üçün isə Stream. AsyncValue hər iki halda eynidir.
Neçə metod? Müqavilə "tətbiqin ehtiyacı" qədər olmalıdır, nə az, nə çox. fetchAll(filter, sort, page, size, includeArchived, ...) kimi 8 parametrli "universal" metod əslində müqaviləni gizlədilmiş SQL-ə çevirir. Əvəzinə niyyət əsaslı metodlar yazmaq daha oxunaqlıdır: fetchMine(), fetchArchived(), search(String query).
Ən çox rast gəlinən səhv: müqaviləni implementasiyadan sonra yazmaq. O zaman müqavilə HTTP-nin güzgüsünə çevrilir (getOrders, postOrder, patchOrderStatus) və heç bir dəyər vermir.
Düzgün sıra: əvvəlcə notifier-in nəyə ehtiyacı olduğunu yaz, ondan müqaviləni çıxar, sonra implementasiya et.
Praktika. Layihəndən bir repository seç və müqaviləsini yenidən yaz: hər metodun adını niyyət əsaslı et, imzalardan bütün kitabxana tiplərini çıxar. Sonra həmin müqavilənin Fake... implementasiyasını yaz (yaddaşdaki siyahı üzərində).
Hazır sayılır: grep -E "Response|Dio|Map<String" lib/domain/repositories heç nə tapmır və fake implementasiya 30 sətirdən qısadır.
📚 Mənbələr və sənədlər
- Arxitektura tövsiyələri: abstract repositoryrəsmidocs.flutter.dev
Abstract repository-nin niyə ən yüksək prioritetli tövsiyə olduğu: müxtəlif mühit üçün müxtəlif implementasiya.
- Dart: sinif modifikatorlarırəsmidart.dev
`abstract interface class`-ın nə verdiyi: `implements` icazəlidir, `extends` qadağandır.
- Offline-first dizayn pattern-irəsmidocs.flutter.dev
Repository-nin `Stream` qaytarmasının niyə üstün olduğu — lokal, sonra serverdən gələn məlumat.
- Case study: data qatırəsmidocs.flutter.dev
Real layihədə repository müqaviləsi və `...Remote` / `...Local` implementasiyaları.