Dependency Injection: wat is het, dependency injectie in iOS en Android

Auteur: IT Sectr Gepubliceerd: 2026-02-18 Leestijd: 9 min

Dependency Injection (DI, dependency injectie) — techniek waarbij een object zijn afhankelijkheden van buitenaf krijgt, in plaats van ze zelf te maken. DI is een implementatie van het IoC-principe (Inversion of Control) en vormt de basis van Dagger, Hilt en Swinject. Dependency injectie vermindert de koppeling van code, vereenvoudigt testen en maakt de architectuur flexibel. In Android is DI standaard via Dagger Hilt van Google, in iOS — via Swinject of handmatige injectie. Meer — in Android DI Guide.

Belangrijkste punten

  • Dependency Injection — afhankelijkheden worden van buitenaf aan het object doorgegeven, niet intern gemaakt
  • Inversion of Control — DI implementeert het IoC-principe, de controle over de flow wordt aan de container overgedragen
  • Dagger Hilt — DI-standaard voor Android, gebaseerd op Dagger van Google
  • Swinject — populair DI-framework voor iOS en Swift
  • Vermindering van koppeling — klasse is afhankelijk van abstracties, niet van concrete implementaties

Wat is Dependency Injection: essentie en soorten DI

Dependency Injection — techniek waarbij een object afhankelijkheden (services, repositories, configuraties) ontvangt via de constructor, setter of interface, in plaats van ze zelf met new te maken. Het doel van DI is het verminderen van koppeling (coupling) tussen klassen. Als een klasse zelf afhankelijkheden maakt, is deze sterk gebonden aan concrete implementaties, wat testen en wijzigen bemoeilijkt. Bij DI werkt de klasse met een abstractie (protocol/interface) en wordt de concrete implementatie van buitenaf geleverd.

Drie manieren van injectie — Constructor Injection (via init/constructor), Setter Injection (via property/setter), Interface Injection (via interfacemethode). Constructor Injection — de voorkeursmanier: afhankelijkheden zijn duidelijk zichtbaar in de handtekening, het object wordt altijd in een geldige staat aangemaakt. Setter Injection wordt gebruikt voor optionele afhankelijkheden met een standaardwaarde. Interface Injection — zelden, voornamelijk voor DI-containers.

DI-typeManierWanneer gebruikenVoorbeeld
ConstructorInitialisatieparametersVerplichte afhankelijkhedeninit(service: ServiceProtocol)
PropertyKlassenpropertyOptionele afhankelijkhedenvar service: ServiceProtocol?
MethodMethodeparameterTijdelijke afhankelijkhedenfunc doWork(with service: Service)

DI-container — bibliotheek die het maken en de levenscyclus van afhankelijkheden beheert. De container bevat registratie van types (elk abstract type wordt gekoppeld aan een concrete implementatie) en een fabriek voor het maken van objecten met opgeloste afhankelijkheden. In Android — Dagger/Hilt, in iOS — Swinject, Needle, Dip. De container kan de scope beheren: singleton (één instantie per applicatie), functiescope (per scherm) of een nieuw object bij elk verzoek.

Dagger Hilt: DI voor Android met codegeneratie

Dagger Hilt — laag bovenop Dagger van Google, de standaard DI-bibliotheek voor Android. Hilt vereenvoudigt Dagger: verwijdert handmatige aanmaak van componenten, voegt @HiltAndroidApp, @AndroidEntryPoint en @Module toe. Hilt integreert met de Android-levenscyclus: ViewModel, Activity, Fragment, Service, BroadcastReceiver kunnen afhankelijkheden ontvangen via annotaties. Codegeneratie vindt plaats tijdens compilatie — Dagger genereert implementaties van componenten, wat zorgt voor nul runtime-overhead.

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

// Module — bepaalt hoe afhankelijkheden worden gemaakt
@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 ontvangt afhankelijkheid via constructor
@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 activeert DI
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    private val viewModel: MainViewModel by viewModels()
}

Dagger-componenten en zichtbaarheidsbereiken — @Singleton (voor de hele applicatie), @ActivityScoped (voor Activity), @FragmentScoped (voor Fragment), @ViewModelScoped (voor ViewModel). De keuze van de scope bepaalt de levensduur van het object. @Singleton — één instantie per proces, geschikt voor OkHttpClient en database. @ActivityScoped — het object leeft zolang de Activity leeft, voor schermafhankelijkheden. @ViewModelScoped — nieuw in Hilt 2.45+, het object leeft zolang de ViewModel leeft, handig voor coroutine-scopes.

Swinject: DI voor iOS in Swift

Swinject — populair DI-framework voor iOS met open source. Swinject biedt Container, Assemblies en verschillende scopes. In tegenstelling tot Dagger werkt Swinject in runtime — afhankelijkheden worden dynamisch opgelost zonder codegeneratie. Dit maakt Swinject eenvoudiger in te stellen, maar moeilijker te debuggen: een fout met een onopgeloste afhankelijkheid verschijnt pas in runtime. Swinject ondersteunt Constructor Injection, Property Injection en Method Injection.

swift
import Swinject

// Assembly — registratiegroep
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 via 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
        }
    }
}

// DI-configuratie in AppDelegate of App
let assembler = Assembler([NetworkAssembly(), ServiceAssembly()])
let viewModel = assembler.resolver.resolve(ProfileViewModel.self)!

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

Swinject-scopes — .transient (elke keer een nieuw object), .container (singleton per container), .graph (standaard — het object wordt gedeeld binnen één afhankelijkheidsgraaf). Voor iOS-applicaties zijn .container en .transient voldoende. Swinject ondersteunt ook Assembler — groepering van Assembly voor modulaire architectuur. Voor tests worden Assembly's vervangen door MockAssembly, wat het mogelijk maakt afhankelijkheden te vervangen zonder de productiecode te wijzigen.

Vergelijking van DI met Service Locator en handmatige injectie

DI vs Service Locator — beide patronen lossen het probleem van afhankelijkheidsbeheer op, maar op verschillende manieren. DI injecteert afhankelijkheden in het object, Service Locator biedt een globaal register waaruit het object zelf afhankelijkheden opvraagt. DI declareert afhankelijkheden expliciet via de constructor (of setter). Service Locator verbergt afhankelijkheden — ze worden binnen de methode opgevraagd, wat de handtekening minder informatief maakt. DI is gemakkelijker te testen: het volstaat om een mock in de constructor te plaatsen. Service Locator vereist configuratie van het globale register voor elke test.

KenmerkDependency InjectionService LocatorHandmatige injectie
Zichtbaarheid van afhankelijkhedenIn de constructorVerborgen in de methodebodyExpliciet
TestenMock in constructorLocator configurerenMock in constructor
Complexiteit van configuratieVereist DI-containerGlobaal registerHandmatige aanmaak
Runtime-overheadDagger — compile-timeRuntime lookupGeen

DI vs handmatige injectie — zonder DI-container worden afhankelijkheden handmatig gemaakt in fabrieken of AppDelegate. Voor 5-10 klassen is handmatige injectie eenvoudiger — het vereist geen studie van Dagger of Swinject. Voor 50+ klassen wordt handmatige injectie een probleem: constructors met 5-6 parameters, complexe aanmaakvolgorde, code-duplicatie. De DI-container automatiseert deze processen en biedt een duidelijke levenscyclus. Handmatige injectie zonder container — een goede keuze voor kleine projecten en prototypes.

Beste praktijken voor Dependency Injection

Constructor Injection — standaard. Gebruik altijd Constructor Injection voor verplichte afhankelijkheden. Dit maakt afhankelijkheden expliciet en het object altijd klaar voor gebruik. Setter Injection — alleen voor optionele afhankelijkheden (bijv. delegate of listener). Interface Injection — niet gebruiken, tenzij u uw eigen DI-bibliotheek schrijft. Constructor Injection — de enige manier om te garanderen dat het object in een geldige staat wordt aangemaakt.

Eén klasse — één verantwoordelijkheid. Als de constructor van een klasse 5+ parameters vereist, schendt de klasse waarschijnlijk het Single Responsibility Principle. Verdeel de klasse in meerdere met minder afhankelijkheden. Signaal: als u een klasse ServiceManager met 6 verschillende services schrijft — is dit het anti-patroon God Object. Haal de bedrijfslogica uit in Use Cases (Interactors), elk met 1-2 afhankelijkheden.

kotlin
// ❌ Slecht: 6 afhankelijkheden — 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
)

// ✅ Goed: Use Cases met 1-2 afhankelijkheden
class ProfileViewModel @Inject constructor(
    private val loadProfileUseCase: LoadProfileUseCase,
    private val trackAnalyticsUseCase: TrackAnalyticsUseCase
)

Scope en levenscyclus — kies de juiste scope voor elke afhankelijkheid. Singletons: OkHttpClient, database, SharedPreferences. Functiescope: repositories, Use Cases (als ze geen status hebben). Transient: Value Objects, DateFormatter, parsers. Scopefout — een veelvoorkomend probleem: een singleton dat de schermstatus opslaat leidt tot geheugenlekken. In Android Hilt lost @ActivityScoped dit probleem op, in Swinject — .container met voorzichtigheid.

Veelgestelde vragen

Waarom is DI nodig als je gewoon new kunt gebruiken?

new creëert een sterke koppeling tussen klassen — u kunt de implementatie niet vervangen zonder de code te wijzigen. Testen is moeilijk: u kunt geen mock plaatsen in plaats van de echte service. SRP wordt geschonden: de klasse is verantwoordelijk voor zowel bedrijfslogica als het maken van afhankelijkheden. DI lost deze problemen op door afhankelijkheden van buitenaf te injecteren en met abstracties te werken.

Dagger Hilt of Koin — wat kiezen voor Android?

Dagger Hilt — de standaard van Google, compile-time DI met codegeneratie, betere prestaties en integratie met Jetpack. Koin — runtime DI, eenvoudiger in te stellen, maar langzamer en met runtime-fouten. Kies Hilt voor productieprojecten. Koin is geschikt voor prototypes en kleine applicaties.

Is Swinject de enige DI voor iOS?

Nee. Voor iOS zijn beschikbaar: Swinject (runtime, populair), Needle (compile-time van Uber), Dip (lichtgewicht), Weaver (gebaseerd op Sourcery). Apple biedt geen ingebouwde DI-container, maar handmatige injectie via init is standaardpraktijk. Voor SwiftUI is handmatige DI via Environment of @StateObject zonder externe bibliotheken vaak voldoende.

Kan DI zonder framework worden gebruikt?

Ja. Handmatige injectie via de constructor — dit is DI zonder framework. Service Locator — een alternatief zonder framework. Fabrieken en Factory Method — ook een vorm van DI. Het framework (Dagger, Swinject) automatiseert routinematige registratie en het oplossen van afhankelijkheden, maar voor 10-20 klassen is handmatige DI voldoende.

Is DI een patroon of een principe?

DI — is een techniek (patroon) die het principe Inversion of Control implementeert. In tegenstelling tot GoF-patronen heeft DI geen strikte structuur van 3-4 klassen. DI is een manier om afhankelijkheden te organiseren, geen ontwerppatroon. DI-containers (Dagger, Swinject) zijn frameworks die deze techniek automatiseren.

Samenvatting

  • DI — techniek van dependency injectie van buitenaf via constructor, setter of methode
  • Dagger Hilt — DI-standaard voor Android met compile-time codegeneratie en @HiltViewModel
  • Swinject — runtime DI voor iOS met Container, Assembly en scopes
  • Constructor Injection — de voorkeursmanier voor verplichte afhankelijkheden
  • Scope — Singleton voor stateless services, functiescope voor schermafhankelijkheden
  • Testen — DI vereenvoudigt het vervangen van afhankelijkheden door mock zonder codewijziging

We ontwikkelen een mobiele applicatie turnkey

IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.

Bespreek het project

Lees ook