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.
// ══ 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.
| Meyar | Riverpod | provider (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ış tip | Kompilyasiya xətası (provider mövcud olmalıdır) | İcra vaxtı xətası | İcra vaxtı xətası |
| Ömür idarəsi | `ref.onDispose`, autoDispose | Widget 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ət | Ayrıdır — state üçün başqa alət lazımdır |
| Widget ağacından asılılıq | Yoxdur (`ProviderContainer` ilə saf Dart-da da işləyir) | Var | Yoxdur |
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-dagetIt.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ığ: httpClient → apiClient → repository 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ı.