Dependency Injection: co to jest, wstrzykiwanie zależności w iOS i Android

Autor: IT Sectr Opublikowano: 2026-02-18 Czas czytania: 9 min

Dependency Injection (DI, wstrzykiwanie zależności) — technika, w której obiekt otrzymuje swoje zależności z zewnątrz, zamiast tworzyć je samodzielnie. DI jest implementacją zasady IoC (Inversion of Control) i leży u podstaw Dagger, Hilt i Swinject. Wstrzykiwanie zależności zmniejsza powiązanie kodu, upraszcza testowanie i czyni architekturę elastyczną. W Android DI — standard przez Dagger Hilt od Google, w iOS — przez Swinject lub ręczne wstrzykiwanie. Więcej — w Android DI Guide.

Najważniejsze

  • Dependency Injection — zależności są przekazywane obiektowi z zewnątrz, a nie tworzone wewnątrz
  • Inversion of Control — DI implementuje zasadę IoC, sterowanie przepływem przekazywane jest kontenerowi
  • Dagger Hilt — standard DI dla Android, oparty na Dagger od Google
  • Swinject — popularny framework DI dla iOS i Swift
  • Zmniejszenie powiązania — klasa zależy od abstrakcji, a nie od konkretnych implementacji

Czym jest Dependency Injection: istota i rodzaje DI

Dependency Injection — technika, w której obiekt otrzymuje zależności (serwisy, repozytoria, konfiguracje) przez konstruktor, setter lub interfejs, zamiast tworzyć je sam przez new. Celem DI jest zmniejszenie powiązania (coupling) między klasami. Jeśli klasa sama tworzy zależności, jest sztywno związana z konkretnymi implementacjami, co utrudnia testowanie i modyfikację. Przy DI klasa pracuje z abstrakcją (protocol/interface), a konkretna implementacja jest dostarczana z zewnątrz.

Trzy sposoby wstrzykiwania — Constructor Injection (przez init/constructor), Setter Injection (przez właściwość/setter), Interface Injection (przez metodę interfejsu). Constructor Injection — preferowany sposób: zależności są jawnie widoczne w sygnaturze, obiekt zawsze tworzony jest w prawidłowym stanie. Setter Injection stosuje się dla opcjonalnych zależności z wartością domyślną. Interface Injection — rzadko, głównie dla kontenerów DI.

Rodzaj DISposóbKiedy stosowaćPrzykład
ConstructorParametry inicjalizatoraObowiązkowe zależnościinit(service: ServiceProtocol)
PropertyWłaściwość klasyOpcjonalne zależnościvar service: ServiceProtocol?
MethodParametr metodyTymczasowe zależnościfunc doWork(with service: Service)

Kontener DI — biblioteka zarządzająca tworzeniem i cyklem życia zależności. Kontener zawiera rejestrację typów (każdy abstrakcyjny typ jest mapowany na konkretną implementację) oraz fabrykę do tworzenia obiektów z rozwiązanymi zależnościami. W Android — Dagger/Hilt, w iOS — Swinject, Needle, Dip. Kontener może zarządzać zakresem (Scope): singleton (jedna instancja na aplikację), zakres funkcji (na ekran) lub nowy obiekt przy każdym żądaniu.

Dagger Hilt: DI dla Android z kodogeneracją

Dagger Hilt — nakładka na Dagger od Google, standardowa biblioteka DI dla Android. Hilt upraszcza Dagger: usuwa ręczne tworzenie komponentów, dodaje @HiltAndroidApp, @AndroidEntryPoint i @Module. Hilt integruje się z cyklem życia Android: ViewModel, Activity, Fragment, Service, BroadcastReceiver mogą otrzymywać zależności przez adnotacje. Kodogeneracja odbywa się na etapie kompilacji — Dagger generuje implementacje komponentów, co daje zerowy narzut runtime.

kotlin
// Application class
@HiltAndroidApp
class MyApp : Application()

// Module — definiuje, jak tworzyć zależności
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    @Provides
    @Singleton
    fun provideOkHttpClient(): OkHttpClient = OkHttpClient.Builder().build()

    @Provides
    @Singleton
    fun provideApiService(client: OkHttpClient): ApiService {
        return Retrofit.Builder()
            .baseUrl("https://api.example.com")
            .client(client)
            .build()
            .create(ApiService::class.java)
    }
}

// ViewModel otrzymuje zależność przez konstruktor
@HiltViewModel
class MainViewModel @Inject constructor(
    private val apiService: ApiService
) : ViewModel() {
    private val _state = MutableStateFlow(MainState.Loading)
    val state: StateFlow<MainState> = _state.asStateFlow()

    fun loadData() {
        viewModelScope.launch {
            _state.value = MainState.Success(apiService.getData())
        }
    }
}

// Activity — @AndroidEntryPoint włącza DI
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    private val viewModel: MainViewModel by viewModels()
}

Komponenty Dagger i zakresy widoczności — @Singleton (na całą aplikację), @ActivityScoped (na Activity), @FragmentScoped (na Fragment), @ViewModelScoped (na ViewModel). Wybór zakresu określa czas życia obiektu. @Singleton — jedna instancja na proces, odpowiednia dla OkHttpClient i bazy danych. @ActivityScoped — obiekt żyje dopóki żyje Activity, dla zależności ekranowych. @ViewModelScoped — nowość Hilt 2.45+, obiekt żyje dopóki żyje ViewModel, co jest wygodne dla zakresów korutyn.

Swinject: DI dla iOS w Swift

Swinject — popularny framework DI dla iOS o otwartym kodzie źródłowym. Swinject udostępnia Container, Assemblies i różne zakresy. W przeciwieństwie do Dagger, Swinject działa w runtime — zależności są rozwiązywane dynamicznie bez kodogeneracji. To czyni Swinject prostszym w konfiguracji, ale sprawia trudności z debugowaniem: błąd nierozwiązanej zależności ujawnia się dopiero w runtime. Swinject obsługuje Constructor Injection, Property Injection i Method Injection.

swift
import Swinject

// Assembly — grupa rejestracji
class NetworkAssembly: Assembly {
    func assemble(container: Container) {
        container.register(NetworkServiceProtocol.self) { _ in
            NetworkService()
        }.inObjectScope(.container) // singleton

        container.register(UserRepositoryProtocol.self) { r in
            UserRepository(
                networkService: r.resolve(NetworkServiceProtocol.self)!
            )
        }
    }
}

// ViewModel przez Constructor Injection
class ProfileViewModel: ObservableObject {
    private let repository: UserRepositoryProtocol

    init(repository: UserRepositoryProtocol) {
        self.repository = repository
    }

    @Published var user: User?
    func loadUser() {
        repository.fetchUser { [weak self] user in
            self?.user = user
        }
    }
}

// Konfiguracja DI w AppDelegate lub App
let assembler = Assembler([NetworkAssembly(), ServiceAssembly()])
let viewModel = assembler.resolver.resolve(ProfileViewModel.self)!

// Property Injection dla UIKit ViewController
container.register(UserViewController.self) { r in
    let vc = UserViewController()
    vc.viewModel = r.resolve(ProfileViewModel.self)
    return vc
}

Zakresy Swinject — .transient (nowy obiekt za każdym razem), .container (singleton na kontener), .graph (domyślnie — obiekt współdzielony w obrębie jednego grafu zależności). Dla aplikacji iOS wystarczające są .container i .transient. Swinject obsługuje również Assembler — grupowanie Assembly dla architektury modułowej. Do testów Assembly są zastępowane przez MockAssembly, co umożliwia podmianę zależności bez zmiany kodu produkcyjnego.

Porównanie DI z Service Locator i ręcznym wstrzykiwaniem

DI vs Service Locator — oba wzorce rozwiązują problem zarządzania zależnościami, ale w różny sposób. DI wstrzykuje zależności do obiektu, Service Locator udostępnia globalny rejestr, z którego obiekt sam pobiera zależności. DI jawnie deklaruje zależności przez konstruktor (lub setter). Service Locator ukrywa zależności — są one pobierane wewnątrz metody, co czyni sygnaturę mniej informacyjną. DI łatwiej testować: wystarczy podstawić mock do konstruktora. Service Locator wymaga konfiguracji globalnego rejestru dla każdego testu.

CechaDependency InjectionService LocatorRęczne wstrzykiwanie
Jawność zależnościW konstruktorzeUkryte w ciele metodyJawne
TestowanieMock w konstruktorzeKonfiguracja LocatorMock w konstruktorze
Złożoność konfiguracjiWymaga kontenera DIGlobalny rejestrRęczne tworzenie
Narzut runtimeDagger — compile-timeRuntime lookupBrak

DI vs ręczne wstrzykiwanie — bez kontenera DI zależności są tworzone ręcznie w fabrykach lub AppDelegate. Dla 5-10 klas ręczne wstrzykiwanie jest prostsze — nie wymaga nauki Dagger ani Swinject. Dla 50+ klas ręczne wstrzykiwanie staje się problemem: konstruktory z 5-6 parametrami, skomplikowana kolejność tworzenia, powielanie kodu. Kontener DI automatyzuje te procesy i zapewnia przejrzysty lifecycle. Ręczne wstrzykiwanie bez kontenera — dobry wybór dla małych projektów i prototypów.

Najlepsze praktyki Dependency Injection

Constructor Injection — standard. Zawsze używaj Constructor Injection dla obowiązkowych zależności. To czyni zależności jawnymi, a obiekt zawsze gotowym do pracy. Setter Injection — tylko dla opcjonalnych zależności (np. delegate lub listener). Interface Injection — nie używaj, chyba że piszesz własną bibliotekę DI. Constructor Injection — jedyny sposób, aby zagwarantować, że obiekt jest tworzony w prawidłowym stanie.

Jedna klasa — jedna odpowiedzialność. Jeśli konstruktor klasy wymaga 5+ parametrów, prawdopodobnie klasa narusza Single Responsibility Principle. Podziel klasę na kilka z mniejszą liczbą zależności. Sygnał: jeśli piszesz klasę ServiceManager z 6 różnymi serwisami — to antywzorzec God Object. Wydziel logikę biznesową do Use Cases (Interactors), każdy z 1-2 zależnościami.

kotlin
// ❌ Źle: 6 zależności — God Object
class ProfileViewModel @Inject constructor(
    private val api: ApiService,
    private val db: Database,
    private val analytics: Analytics,
    private val prefs: Preferences,
    private val location: LocationProvider,
    private val notification: NotificationManager
)

// ✅ Dobrze: Use Cases z 1-2 zależnościami
class ProfileViewModel @Inject constructor(
    private val loadProfileUseCase: LoadProfileUseCase,
    private val trackAnalyticsUseCase: TrackAnalyticsUseCase
)

Zakres i lifecycle — wybieraj odpowiedni zakres dla każdej zależności. Singleton: OkHttpClient, baza danych, SharedPreferences. Zakres funkcji: repozytoria, Use Cases (jeśli nie mają stanu). Transient: Value Objects, DateFormatter, parsery. Błąd zakresu — częsty problem: singleton przechowujący stan ekranu prowadzi do wycieków. W Android Hilt @ActivityScoped rozwiązuje ten problem, w Swinject — .container z ostrożnością.

Często zadawane pytania

Po co potrzebny jest DI, skoro można po prostu tworzyć new?

new tworzy sztywne powiązanie między klasami — nie możesz podmienić implementacji bez zmiany kodu. Trudno testować: nie można wstawić mocka zamiast prawdziwego serwisu. Naruszane jest SRP: klasa odpowiada zarówno za logikę biznesową, jak i za tworzenie zależności. DI rozwiązuje te problemy poprzez wstrzykiwanie zależności z zewnątrz i pracę z abstrakcjami.

Dagger Hilt czy Koin — co wybrać dla Android?

Dagger Hilt — standard od Google, compile-time DI z kodogeneracją, lepsza wydajność i integracja z Jetpack. Koin — runtime DI, prostszy w konfiguracji, ale wolniejszy i z błędami w runtime. Wybieraj Hilt dla projektów produkcyjnych. Koin nadaje się do prototypów i małych aplikacji.

Czy Swinject to jedyne DI dla iOS?

Nie. Dla iOS dostępne są: Swinject (runtime, popularny), Needle (compile-time od Uber), Dip (lekki), Weaver (oparty na Sourcery). Apple nie udostępnia wbudowanego kontenera DI, ale ręczne wstrzykiwanie przez init to standardowa praktyka. Dla SwiftUI często wystarcza ręczne DI przez Environment lub @StateObject bez zewnętrznych bibliotek.

Czy można używać DI bez frameworka?

Tak. Ręczne wstrzykiwanie przez konstruktor — to DI bez frameworka. Service Locator — alternatywa bez frameworka. Fabryki i Factory Method — również forma DI. Framework (Dagger, Swinject) automatyzuje rutynową rejestrację i rozwiązywanie zależności, ale dla 10-20 klas ręczne DI jest wystarczające.

Czy DI to wzorzec czy zasada?

DI — to technika (wzorzec) implementująca zasadę Inversion of Control. W przeciwieństwie do wzorców GoF, DI nie ma ścisłej struktury z 3-4 klas. DI — sposób organizacji zależności, a nie wzorzec projektowania. Kontenery DI (Dagger, Swinject) — to frameworki automatyzujące tę technikę.

Podsumowanie

  • DI — technika wstrzykiwania zależności z zewnątrz przez konstruktor, setter lub metodę
  • Dagger Hilt — standard DI dla Android z compile-time kodogeneracją i @HiltViewModel
  • Swinject — runtime DI dla iOS z Container, Assembly i zakresami
  • Constructor Injection — preferowany sposób dla obowiązkowych zależności
  • Zakres — Singleton dla bezstanowych serwisów, zakres funkcji dla zależności ekranowych
  • Testowanie — DI upraszcza podmianę zależności na mock bez zmiany kodu

Opracujemy aplikację mobilną pod klucz

IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.

Omów projekt

Przeczytaj również