Two implementations: remote, local and fake (flavors)
This is the real reason for an abstract repository: one contract, several implementations. The official recommendations put it this way — abstract repository classes let you write a different implementation for different environments.
In practice there are four kinds:
- `...Remote` — production: the real API.
- `...Local` — a local database or canned data: demo mode, offline showcases, working in parallel while the backend is unfinished.
- `...Fake` (in the test folder) — an in-memory list: unit and widget tests.
- `...Delegating` (rare) — a layer that picks between implementations at run time.
The most valuable aspect of this setup is teamwork: while a backend endpoint does not exist yet, the mobile developer builds the entire screen, every state and every error case against a ...Local implementation. When the endpoint lands, only one line in a provider changes.
// ══ 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);
}
}One contract, three implementations. `...Local` is not only for demos — it also simulates errors and latency.
// ══ 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.Flavors: the official case study uses separate entry points (`main_development.dart`, `main_staging.dart`). In Riverpod the difference is the `overrides` list.
| Implementation | Location | When it is used | Its distinctive trait |
|---|---|---|---|
| `...Remote` | `lib/data/repositories/` | Production | Real services, caching, retries |
| `...Local` | `lib/data/repositories/` | Demos, offline showcases, waiting on the backend | Latency and failure rate are configurable |
| `...Fake` | `test/fakes/` | Unit and widget tests | No latency; it counts calls |
| `...Delegating` | `lib/data/repositories/` | Choosing at run time (a feature flag, say) | Rare — usually solved at the DI level instead |
Making latency and failure rate configurable in a ...Local implementation looks like a small detail, but its practical value is large: the loading spinner, the empty-list screen, the error screen and the retry button only get exercised for real this way. When data returns instantly, the loading state is never seen and its bugs stay hidden until production.
Practice. Write a ...Local implementation for one repository (with latency and a failure rate) and create a lib/main_development.dart entry point. Then run the app with flutter run -t lib/main_development.dart and see three states with your own eyes: loading, empty list, error plus retry.
Done means: all three states appear on a real screen, and there is not a single if (isDev) condition in the app code.
📚 Sources and documentation
- Architecture recommendations: abstract repositoriesofficialdocs.flutter.dev
A different implementation per environment — the official justification for abstract repositories.
- Case study: dependency injectionofficialdocs.flutter.dev
Choosing the implementation at the entry point and casting it to the abstract type.
- Flutter: flavorsofficialdocs.flutter.dev
Setting up flavors at platform level — separate bundle ids, icons and configuration.
- Riverpod: testing and overridesofficialriverpod.dev
`ProviderScope(overrides: …)` and `ProviderContainer.test()` — replacing an implementation.