İki implementasiya: remote, local və fake (flavor-lar)
Abstract repository-nin əsl səbəbi budur: bir müqavilə, bir neçə implementasiya. Rəsmi tövsiyələr bunu belə formalaşdırır — abstract repository sinifləri müxtəlif mühitlər üçün müxtəlif implementasiya yazmağa imkan verir.
Praktikada dörd implementasiya növü olur:
- `...Remote` — məhsul rejimi: real API.
- `...Local` — lokal baza ya da hazır məlumat: demo rejimi, offline nümayiş, backend hazır olmayanda paralel iş.
- `...Fake` (test qovluğunda) — yaddaşda saxlanan siyahı: unit və widget testləri.
- `...Delegating` (nadir) — bir neçə implementasiyanı şəraitə görə seçən qat.
Bu quruluşun ən dəyərli tərəfi komanda işidir: backend endpoint-i hazır olmadıqda mobil developer ...Local implementasiyası ilə bütün ekranı, bütün state-ləri və bütün xəta hallarını qurur. Endpoint gəldikdə yalnız provider-in bir sətri dəyişir.
// ══ Müqavilə (domain) ══
abstract interface class ProductRepository {
Future<List<Product>> fetchActive();
Future<Product> fetchById(String id);
}
// ══ 1. Məhsul rejimi ══
class ProductRepositoryRemote implements ProductRepository {
ProductRepositoryRemote({required ProductApiClient apiClient})
: _apiClient = apiClient;
final ProductApiClient _apiClient;
@override
Future<List<Product>> fetchActive() async =>
(await _apiClient.getActiveProducts())
.map((dto) => dto.toDomain())
.toList();
@override
Future<Product> fetchById(String id) async =>
(await _apiClient.getProduct(id)).toDomain();
}
// ══ 2. Demo / backend hazır olmayanda ══
class ProductRepositoryLocal implements ProductRepository {
ProductRepositoryLocal({
this.latency = const Duration(milliseconds: 600),
this.failureRate = 0,
});
/// Real şəbəkə gecikməsini imitasiya edir — loading state-ini
/// GÖRMƏK üçün vacibdir, əks halda spinner heç vaxt görünmür.
final Duration latency;
/// 0..1 — xəta ehtimalı. Xəta ekranını sınamaq üçün.
final double failureRate;
static const _seed = [
Product(id: 'p1', title: 'Yaşıl çay', price: 8.5, discountPercent: 10),
Product(id: 'p2', title: 'Qara çay', price: 6),
Product(id: 'p3', title: 'Arxivdə', price: 4, isArchived: true),
];
@override
Future<List<Product>> fetchActive() async {
await Future<void>.delayed(latency);
if (_shouldFail()) throw const NetworkFailure();
return _seed.where((p) => !p.isArchived).toList();
}
@override
Future<Product> fetchById(String id) async {
await Future<void>.delayed(latency);
if (_shouldFail()) throw const NetworkFailure();
return _seed.firstWhere(
(p) => p.id == id,
orElse: () => throw const NotFoundFailure(),
);
}
bool _shouldFail() =>
failureRate > 0 && Random().nextDouble() < failureRate;
}
// ══ 3. Test (test/fakes/) ══
class FakeProductRepository implements ProductRepository {
FakeProductRepository(this.products, {this.error});
final List<Product> products;
final Object? error; // xəta yolunu sınamaq üçün
int fetchActiveCount = 0;
@override
Future<List<Product>> fetchActive() async {
fetchActiveCount++;
if (error != null) throw error!;
return products; // gecikmə YOX — test sürətli olsun
}
@override
Future<Product> fetchById(String id) async {
if (error != null) throw error!;
return products.firstWhere((p) => p.id == id);
}
}Bir müqavilə, üç implementasiya. `...Local` yalnız demo üçün deyil — xəta və gecikmə hallarını da simulyasiya edir.
// ══ lib/main.dart ══ (məhsul rejimi)
void main() {
runApp(const ProviderScope(child: MyApp()));
}
// ══ lib/main_development.dart ══ (lokal məlumatla)
void main() {
runApp(
ProviderScope(
overrides: [
// Bütün data qatı bir siyahı ilə əvəz olunur.
productRepositoryProvider.overrideWithValue(
ProductRepositoryLocal(
latency: const Duration(milliseconds: 800),
failureRate: 0.2, // hər 5 sorğudan biri xəta
),
),
orderRepositoryProvider.overrideWithValue(
OrderRepositoryLocal(seed: demoOrders),
),
],
child: const MyApp(),
),
);
}
// ══ lib/main_staging.dart ══ (real API, başqa baseUrl)
void main() {
runApp(
ProviderScope(
overrides: [
apiBaseUrlProvider.overrideWithValue('https://staging.example.com'),
// Ödəniş isə staging-də də fake olur — real pul hərəkəti olmasın.
paymentRepositoryProvider.overrideWithValue(
PaymentRepositoryFake(alwaysSucceed: true),
),
],
child: const MyApp(),
),
);
}
// İşə salmaq:
// flutter run -t lib/main_development.dart
// flutter run -t lib/main_staging.dart --flavor staging
//
// DİQQƏT: bütün overrides ProviderScope-dadır, tətbiq kodunda
// heç bir `if (isDev)` şərti yoxdur — bu, ən vacib nəticədir.Flavor-lar: rəsmi case study ayrı giriş nöqtələri işlədir (`main_development.dart`, `main_staging.dart`). Riverpod-da fərq `overrides` siyahısındadır.
| İmplementasiya | Yeri | Nə vaxt işlədilir | Xüsusiyyəti |
|---|---|---|---|
| `...Remote` | `lib/data/repositories/` | Məhsul rejimi | Real service-lər, cache, retry |
| `...Local` | `lib/data/repositories/` | Demo, offline nümayiş, backend gözlənilərkən | Gecikmə və xəta ehtimalı konfiqurasiya olunur |
| `...Fake` | `test/fakes/` | Unit və widget testləri | Gecikmə yoxdur; çağırış sayı sayılır |
| `...Delegating` | `lib/data/repositories/` | Şəraitə görə seçim (məsələn feature flag) | Nadir — çox vaxt DI səviyyəsində həll olunur |
...Local implementasiyasında gecikmə və xəta ehtimalını konfiqurasiya etmək kiçik detal kimi görünür, lakin praktik dəyəri böyükdür: loading spinner-i, boş siyahı ekranı, xəta ekranı və retry düyməsi yalnız bu şəkildə real olarak sınanır. Şəbəkə dərhal cavab verdiyində loading state-i heç vaxt görünmür və onun baqları məhsul rejiminə qədər gizli qalır.
Praktika. Bir repository üçün ...Local implementasiyası yaz (gecikmə + xəta ehtimalı ilə) və lib/main_development.dart giriş nöqtəsi yarat. Sonra tətbiqi flutter run -t lib/main_development.dart ilə işə sal və üç halı öz gözünlə yoxla: loading, boş siyahı, xəta + retry.
Hazır sayılır: üç hal da real ekranda görünür və tətbiq kodunda heç bir if (isDev) şərti yoxdur.
📚 Mənbələr və sənədlər
- Arxitektura tövsiyələri: abstract repositoryrəsmidocs.flutter.dev
Müxtəlif mühitlər üçün müxtəlif implementasiya — abstract repository-nin rəsmi əsaslandırması.
- Case study: dependency injectionrəsmidocs.flutter.dev
İmplementasiyanın giriş nöqtəsində seçilməsi və abstract tipə cast edilməsi.
- Flutter: flavor-larrəsmidocs.flutter.dev
Platforma səviyyəsində flavor qurulması — ayrı bundle id, ikon və konfiqurasiya.
- Riverpod: test və overriderəsmiriverpod.dev
`ProviderScope(overrides: ...)` və `ProviderContainer.test()` — implementasiyanın əvəzlənməsi.