Sparround

DI: constructor injection, Riverpod və get_it müqayisəsi

Dependency injection rəsmi tövsiyələrdə ən yüksək prioritetli bənddir və səbəbi konkretdir: qlobal əlçatan obyektlərdən qaçmaq. Qlobal obyekt (singleton, static sahə) iki problem yaradır — testdə əvəz edilə bilmir və kim onu dəyişdirdiyini izləmək mümkün olmur.

DI-ın iki müstəqil hissəsi var və onları qarışdırmamaq vacibdir:

1. Asılılığın qəbulu (constructor injection). Sinif özünə lazım olanı kənardan qəbul edir və private saxlayır:

ProductRepositoryRemote({required ProductApiClient apiClient}) : _apiClient = apiClient;

Bu, hər hansı DI alətindən asılı deyil — sadə Dart-dır. Rəsmi case study bu üsulu işlədir və əlavə qeyd edir: asılılıqları private saxlamaq lazımdır ki, view onlara birbaşa müraciət edə bilməsin.

2. Qrafın qurulması (wiring). Kimin nəyi kimə ötürdüyünə qərar verən yer. Burada alət seçimi başlayır: rəsmi bələdçi package:provider işlədir, bu branch-ın stack-i Riverpod-dur, cəmiyyət şablonlarında isə get_it + injectable yayılıb.

Vacib nəticə: birinci hissə dəyişmir. Alət dəyişdikdə yalnız qrafın qurulduğu fayllar dəyişir; repository, service və use-case-lər toxunulmur.

dart
// ══ Bu hissə HƏR ÜÇ halda eynidir ══
class ProductApiClient {
  ProductApiClient({required http.Client client}) : _client = client;
  final http.Client _client;                      // private
}

class ProductRepositoryRemote implements ProductRepository {
  ProductRepositoryRemote({required ProductApiClient apiClient})
      : _apiClient = apiClient;
  final ProductApiClient _apiClient;              // private
}


// ══════ 1. RIVERPOD (bu branch-ın stack-i) ══════
// Qraf provider-lərin bir-birini `watch` etməsi ilə qurulur.
@riverpod
http.Client httpClient(Ref ref) {
  final client = http.Client();
  ref.onDispose(client.close);                    // təmizləmə də burada
  return client;
}

@riverpod
ProductApiClient productApiClient(Ref ref) =>
    ProductApiClient(client: ref.watch(httpClientProvider));

@riverpod
ProductRepository productRepository(Ref ref) =>
    ProductRepositoryRemote(apiClient: ref.watch(productApiClientProvider));

// Testdə / demo rejimində:
// ProviderScope(overrides: [
//   productRepositoryProvider.overrideWithValue(FakeProductRepository([])),
// ])


// ══════ 2. PROVIDER (rəsmi bələdçinin üsulu) ══════
// Qraf widget ağacının kökündə qurulur.
void main() {
  runApp(
    MultiProvider(
      providers: [
        Provider(create: (_) => http.Client()),
        Provider(create: (context) =>
            ProductApiClient(client: context.read())),
        // Abstract tipə cast: istifadə yerləri müqaviləni görür.
        Provider<ProductRepository>(
          create: (context) =>
              ProductRepositoryRemote(apiClient: context.read()),
        ),
      ],
      child: const MyApp(),
    ),
  );
}


// ══════ 3. GET_IT (service locator) ══════
// Qraf ayrı funksiyada, tətbiq başlamazdan əvvəl qurulur.
final getIt = GetIt.instance;

void configureDependencies() {
  getIt.registerLazySingleton<http.Client>(() => http.Client());
  getIt.registerLazySingleton<ProductApiClient>(
    () => ProductApiClient(client: getIt<http.Client>()),
  );
  getIt.registerLazySingleton<ProductRepository>(
    () => ProductRepositoryRemote(apiClient: getIt<ProductApiClient>()),
  );
}

void main() {
  configureDependencies();
  runApp(const MyApp());
}

// İstifadə: getIt<ProductRepository>()
// Testdə: getIt.reset() + yenidən qeydiyyat

// ── Ən vacib müşahidə ──
// Yuxarıdaki üç blokdan hansını seçsən, `ProductRepositoryRemote`
// sinfinin kodu DƏYİŞMİR. Bu, DI-ın düzgün qurulduğunun əlamətidir.

Eyni asılılıq qrafı üç alətlə. Diqqət et: repository və service kodu üç halda da eynidir — dəyişən yalnız qrafın qurulduğu yerdir.

MeyarRiverpodprovider (rəsmi)get_it (+ injectable)
Asılılıq necə tapılır`ref.watch/read` — tip təhlükəsiz`context.read` — widget ağacı lazımdır`getIt<T>()` — hər yerdən, kontekst lazım deyil
Qeyd olunmamış tipKompilyasiya xətası (provider mövcud olmalıdır)İcra vaxtı xətasıİcra vaxtı xətası
Ömür idarəsi`ref.onDispose`, autoDisposeWidget ağacına bağlıƏl ilə (`registerLazySingleton`, `reset`)
Testdə əvəzləmə`overrides` — izolyasiya edilmişWidget ağacında provider əvəzləmək`getIt.reset()` — qlobal state, testlər arası sızma riski
State management ilə əlaqəEyni alət (provider-lər həm DI, həm state)Eyni alətAyrıdır — state üçün başqa alət lazımdır
Widget ağacından asılılıqYoxdur (`ProviderContainer` ilə saf Dart-da da işləyir)VarYoxdur

Service locator vs injection: əsl fərq. get_it bir service locator-dur: sinif özü getIt<ProductRepository>() çağırıb asılılığı tapır. Constructor injection-da isə asılılıq siniflə verilir.

Fərq praktikdir:

  • Service locator-da sinfin imzası asılılıqları gizlədir: konstruktora baxıb nəyə ehtiyac olduğunu bilmirsən; getIt çağırışını kodun içində axtarmalısan.
  • Testdə service locator qlobal state-dir: bir test qeydiyyatı dəyişdirsə, digər testə sızır. Ona görə setUp-da getIt.reset() intizamı tələb olunur.

Ən yaxşı praktika: `get_it` işlədilsə də, siniflər asılılığı konstruktorda qəbul etməlidir. getIt yalnız qrafın qurulduğu yerdə (registration funksiyasında) çağırılır, sinifin daxilində yox. injectable paketi məhz bunu avtomatlaşdırır — annotasiyalara görə registration kodunu generasiya edir.

Riverpod-da tələ. Provider-lər qlobal dəyişənlərdir, ona görə "bu, qlobal state deyilmi?" sualı yaranır. Cavab: provider elanı qlobaldır, lakin dəyəri ProviderContainer/ProviderScope-a bağlıdır. Ona görə hər test öz konteynerini yaradır və izolyasiya təmin olunur — getIt-dən əsas fərq budur.

Nə vaxt `get_it`? İki real ssenari: (1) layihədə state management üçün BLoC işlədilir və DI üçün ayrı alət lazımdır; (2) DI Flutter-dən kənarda da lazımdır (paylaşılan Dart paketi, CLI). Riverpod işlədilirsə, ikinci DI aləti gətirmək əlavə mürəkkəblikdir.

Asılılıqları private saxlamaq. Rəsmi case study bunu açıq qeyd edir: repository sinfində final ProductApiClient _apiClient; — alt xətt ilə. Səbəb: view model-ə çıxışı olan view repository-nin metodlarını birbaşa çağıra bilməsin. Public sahə qat bölgüsündə deşik yaradır.

Praktika. Layihəndə DI qrafını bir yerə yığ: httpClientapiClientrepository zəncirini provider-lərlə qur (ya da mövcud qurğunu audit et). Sonra iki şeyi yoxla:

1. Hər repository/service sinfi asılılığı konstruktorda qəbul edirmi və onu private saxlayırmı? 2. grep -rn "GetIt\|getIt<" lib — nəticə yalnız registration faylını göstərməlidir, sinif gövdələrini yox.

Hazır sayılır: testdə bir provider override etməklə bütün data qatını əvəz edə bilirsən, və heç bir sinif getIt-i öz içindən çağırmır.

📚 Mənbələr və sənədlər

  • Case study: dependency injectionrəsmidocs.flutter.dev

    Rəsmi yanaşma: `MultiProvider` ilə qrafın qurulması, abstract tipə cast və asılılıqların private saxlanması.

  • Arxitektura tövsiyələri: DIrəsmidocs.flutter.dev

    DI-ın niyə ən yüksək prioritetli tövsiyə olduğu: qlobal obyektlərdən qaçmaq.

  • get_it paketirəsmipub.dev

    `registerSingleton`, `registerLazySingleton`, `registerFactory` və service locator anlayışı.

  • injectable paketirəsmipub.dev

    `get_it` üçün registration kodunun annotasiyalardan generasiyası.