Sparround

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.

dart
// ══ 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ətServiceRepository
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ıdaki ApiClient kimi).
  • 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.