Service: xarici dünya ilə yeganə təmas nöqtəsi
Service bir xarici mənbəni örtür: REST API, lokal baza, fayl sistemi, platforma kanalı. Rəsmi bələdçinin təsvirində onun ən vacib xüsusiyyəti var — state saxlamır.
Service-in məsuliyyətləri:
- Endpoint-lər, başlıqlar, query parametrləri, timeout.
- Sorğunun icrası və status kodunun yoxlanılması.
- Cavabın DTO-ya çevrilməsi (
fromJson). - Uğursuzluqda tipli exception atmaq (
UnauthorizedException,ServerException).
Service-in məsuliyyəti olmayan şeylər: cache, təkrar cəhd siyasəti, domain modelinə çevirmə, iki mənbənin birləşdirilməsi. Bunlar repository-nin işidir.
Bu bölgünün praktik səbəbi: service nazik və dəyişməz qalanda onu asanlıqla test etmək və əvəz etmək olur. Repository-də isə qərarlar cəmlənir — cache nə qədər yaşayır, xəta halında nə edilir, hansı mənbədən oxunur.
// ══ lib/data/services/api_client.dart ══ (ortaq nazik qat)
class ApiClient {
ApiClient({
required http.Client client,
required String baseUrl,
required Future<String?> Function() tokenProvider,
}) : _client = client,
_baseUrl = baseUrl,
_tokenProvider = tokenProvider;
final http.Client _client;
final String _baseUrl;
final Future<String?> Function() _tokenProvider;
Future<Map<String, dynamic>> getJson(
String path, {
Map<String, String>? query,
}) async {
final token = await _tokenProvider();
final response = await _client
.get(
Uri.parse('$_baseUrl$path').replace(queryParameters: query),
headers: {
'Accept': 'application/json',
if (token != null) 'Authorization': 'Bearer $token',
},
)
.timeout(const Duration(seconds: 20));
// Status kodunun TİPLİ exception-a çevrilməsi burada baş verir.
return switch (response.statusCode) {
200 || 201 => jsonDecode(response.body) as Map<String, dynamic>,
401 => throw const UnauthorizedException(),
403 => throw const ForbiddenException(),
404 => throw const NotFoundException(),
>= 500 => throw ServerException(response.statusCode),
_ => throw ApiException(response.statusCode, response.body),
};
}
}
// ══ lib/data/services/order_api_client.dart ══ (bir endpoint qrupu)
class OrderApiClient {
OrderApiClient({required ApiClient api}) : _api = api;
final ApiClient _api;
// Giriş: sadə tiplər. Çıxış: DTO. State: YOXDUR.
Future<List<OrderDto>> getOrders({int page = 1, int limit = 20}) async {
final json = await _api.getJson('/v1/orders', query: {
'page': '$page',
'limit': '$limit',
});
return (json['data'] as List<dynamic>)
.map((e) => OrderDto.fromJson(e as Map<String, dynamic>))
.toList();
}
Future<OrderDto> getOrder(String id) async =>
OrderDto.fromJson(await _api.getJson('/v1/orders/$id'));
}Ortaq `ApiClient` və bir endpoint qrupu üçün service. `http.Client` konstruktordan gəlir — testin yeganə giriş nöqtəsi budur.
| Məsuliyyət | Service | Repository |
|---|---|---|
| URL, başlıqlar, timeout | ✅ | — |
| Status kodunun yoxlanılması | ✅ | — |
| JSON → DTO | ✅ | — |
| DTO → domain modeli | — | ✅ |
| Cache və onun ömrü | — | ✅ |
| Təkrar cəhd (retry) siyasəti | — | ✅ |
| İki mənbənin birləşdirilməsi | — | ✅ |
| State saxlamaq | ❌ Heç vaxt | ✅ Cache, sessiya |
`http`, yoxsa `dio`? Arxitektura baxımından fərq yoxdur — hər ikisi service-in içində qalır və müqavilədə görünmür. Praktik fərq:
http— Dart komandasının paketi, minimal API. Interceptor anlayışı yoxdur; token əlavə etmək üçün öz nazik qatını yazırsan (yuxarıdakiApiClientkimi).dio— interceptor-lar,CancelToken,FormData, yükləmə progress-i hazır gəlir. Böyük layihələrdə bu, vaxt qazandırır.
Seçim nə olursa olsun, iki qayda dəyişmir:
1. Kitabxananın tipləri (Response, DioException) service-dən kənara çıxmır.
2. HTTP client konstruktordan ötürülür — belə olduqda testdə MockClient (paket: http/testing) ya da fake ötürmək mümkün olur.
Auth token. Token-i service-in özündə saxlamaq state yaratmaq deməkdir. Rəsmi case study-də sessiya AuthRepository-də yaşayır; service isə token-i funksiya vasitəsilə oxuyur (yuxarıdaki tokenProvider). Belə olduqda service state saxlamır və token yenilənməsi bir yerdə idarə olunur.
Ən çox rast gəlinən səhv: service-i "repository kimi" işlətmək — yəni ProductService sinfində həm HTTP çağırışı, həm cache, həm domain modelinə çevirmə saxlamaq. Bu, iki qatı birləşdirir və nəticəsi dərhal görünmür; problem offline dəstəyi ya da ikinci mənbə lazım olduqda üzə çıxır: eyni sinifə şəbəkə, baza və cache məntiqi yığılır.
Ad seçimi bu səhvi azaldır: ...ApiClient, ...DatabaseService, ...PreferencesService — ad mənbəni deyir və sinfin "nazik" qalmasını xatırladır.
Praktika. Layihəndəki bir şəbəkə sinfini iki hissəyə ayır: nazik ...ApiClient (endpoint + DTO) və ...RepositoryRemote (cache + domain). Sonra http.Client-i konstruktora çıxar və MockClient ilə bir test yaz.
Hazır sayılır: test şəbəkəyə çıxmır, və grep -rn "http\.\|Dio(" lib/data/repositories heç nə tapmır.
📚 Mənbələr və sənədlər
- Case study: data qatırəsmidocs.flutter.dev
Service və repository bölgüsünün real kodda görünüşü, o cümlədən auth service-i.
- Flutter: şəbəkə sorğularırəsmidocs.flutter.dev
`http` paketi ilə əsas iş — sorğu, status kodu, `fromJson`.
- `http` paketirəsmipub.dev
`Client` abstraksiyası və `http/testing` içindəki `MockClient` — testdə şəbəkəni əvəz etmək üçün.
- `dio` paketirəsmipub.dev
Interceptor-lar, `CancelToken` və digər imkanlar — böyük layihələrdə alternativ.