Dependency Injection: cos'è, iniezione delle dipendenze in iOS e Android

Autore: IT Sectr Pubblicato: 2026-02-18 Tempo di lettura: 9 min

Iniezione delle dipendenze (DI) è una tecnica per cui un oggetto riceve le sue dipendenze dall'esterno anziché crearle autonomamente. La DI è un'implementazione del principio IoC (Inversione del Controllo) ed è alla base di Dagger, Hilt e Swinject. L'iniezione delle dipendenze riduce l'accoppiamento del codice, semplifica i test e rende l'architettura flessibile. Su Android, la DI è standard tramite Dagger Hilt di Google; su iOS, tramite Swinject o iniezione manuale. Maggiori informazioni nella Guida DI Android.

Punti chiave

  • Iniezione delle dipendenze — le dipendenze vengono passate all'oggetto dall'esterno, non create internamente
  • Inversione del Controllo — la DI implementa il principio IoC, il flusso di controllo viene trasferito al contenitore
  • Dagger Hilt — lo standard DI per Android, basato su Dagger di Google
  • Swinject — un popolare framework DI per iOS e Swift
  • Accoppiamento ridotto — una classe dipende da astrazioni, non da implementazioni concrete

Cos'è Dependency Injection: essenza e tipi di DI

Iniezione delle dipendenze è una tecnica per cui un oggetto riceve dipendenze (servizi, repository, configurazioni) tramite un costruttore, setter o interfaccia, anziché crearle lui stesso con new. L'obiettivo della DI è ridurre l'accoppiamento tra le classi. Se una classe crea le dipendenze da sola, è fortemente legata a implementazioni specifiche, rendendo test e modifiche difficili. Con la DI, la classe lavora con un'astrazione (protocollo/interfaccia) e l'implementazione concreta viene fornita dall'esterno.

Tre modalità di iniezione — Iniezione tramite costruttore (via init/constructor), Iniezione tramite setter (via proprietà/setter), Iniezione tramite interfaccia (via un metodo dell'interfaccia). L'iniezione tramite costruttore è il metodo preferito: le dipendenze sono chiaramente visibili nella firma e l'oggetto viene sempre creato in uno stato valido. L'iniezione tramite setter viene utilizzata per dipendenze opzionali con valore predefinito. L'iniezione tramite interfaccia è rara, principalmente per i contenitori DI.

Tipo di DIMetodoQuando usarloEsempio
CostruttoreParametri dell'inizializzatoreDipendenze obbligatorieinit(service: ServiceProtocol)
ProprietàProprietà della classeDipendenze opzionalivar service: ServiceProtocol?
MetodoParametro del metodoDipendenze temporaneefunc doWork(with service: Service)

Contenitore DI — una libreria che gestisce la creazione e il ciclo di vita delle dipendenze. Il contenitore contiene registrazioni di tipi (ogni tipo astratto mappato a un'implementazione concreta) e una factory per creare oggetti con dipendenze risolte. Su Android — Dagger/Hilt, su iOS — Swinject, Needle, Dip. Il contenitore può gestire l'ambito: singleton (un'istanza per applicazione), ambito di funzionalità (per schermata) o un nuovo oggetto a ogni richiesta.

Dagger Hilt: DI per Android con generazione di codice

Dagger Hilt è un wrapper su Dagger di Google, la libreria DI standard per Android. Hilt semplifica Dagger: rimuove la creazione manuale dei componenti, aggiunge @HiltAndroidApp, @AndroidEntryPoint e @Module. Hilt si integra con il ciclo di vita di Android: ViewModel, Activity, Fragment, Service, BroadcastReceiver possono ricevere dipendenze tramite annotazioni. La generazione del codice avviene in fase di compilazione — Dagger genera implementazioni dei componenti, con overhead runtime pari a zero.

kotlin
// Classe dell'applicazione
@HiltAndroidApp
class MyApp : Application()

// Modulo — definisce come creare le dipendenze
@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 riceve la dipendenza tramite costruttore
@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 abilita la DI
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    private val viewModel: MainViewModel by viewModels()
}

Componenti e ambiti di Dagger — @Singleton (intera applicazione), @ActivityScoped (per Activity), @FragmentScoped (per Fragment), @ViewModelScoped (per ViewModel). La scelta dell'ambito determina la durata dell'oggetto. @Singleton — un'istanza per processo, adatto per OkHttpClient e database. @ActivityScoped — l'oggetto vive finché vive l'Activity, per dipendenze a livello di schermata. @ViewModelScoped — novità di Hilt 2.45+, l'oggetto vive finché vive il ViewModel, comodo per ambiti di coroutine.

Swinject: DI per iOS in Swift

Swinject è un popolare framework DI open source per iOS. Swinject fornisce Container, Assemblies e vari ambiti. A differenza di Dagger, Swinject funziona a runtime — le dipendenze vengono risolte dinamicamente senza generazione di codice. Questo rende Swinject più facile da configurare, ma il debug è più difficile: un errore di dipendenza non risolta appare solo a runtime. Swinject supporta l'iniezione tramite costruttore, l'iniezione tramite proprietà e l'iniezione tramite metodo.

swift
import Swinject

// Assembly — un gruppo di registrazioni
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 tramite iniezione del costruttore
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
        }
    }
}

// Configurazione della DI in AppDelegate o App
let assembler = Assembler([NetworkAssembly(), ServiceAssembly()])
let viewModel = assembler.resolver.resolve(ProfileViewModel.self)!

// Iniezione della proprietà per UIKit ViewController
container.register(UserViewController.self) { r in
    let vc = UserViewController()
    vc.viewModel = r.resolve(ProfileViewModel.self)
    return vc
}

Ambiti di Swinject — .transient (nuovo oggetto ogni volta), .container (singleton per contenitore), .graph (predefinito — l'oggetto è condiviso all'interno di un singolo grafo di dipendenze). Per le applicazioni iOS, .container e .transient sono sufficienti. Swinject supporta anche Assembler — raggruppamento di Assembly per un'architettura modulare. Per i test, Assembly viene sostituita con MockAssembly, consentendo la sostituzione delle dipendenze senza modificare il codice di produzione.

Confronto tra DI, Service Locator e iniezione manuale

DI vs Service Locator — entrambi i pattern risolvono la gestione delle dipendenze, ma in modo diverso. La DI inietta le dipendenze nell'oggetto; Service Locator fornisce un registro globale da cui l'oggetto richiede le dipendenze da solo. La DI dichiara esplicitamente le dipendenze tramite il costruttore (o setter). Service Locator nasconde le dipendenze — vengono richieste all'interno del metodo, rendendo la firma meno informativa. La DI è più facile da testare: basta passare un mock al costruttore. Service Locator richiede la configurazione del registro globale per ogni test.

CaratteristicaIniezione delle dipendenzeService LocatorIniezione manuale
Visibilità delle dipendenzeNel costruttoreNascoste nel corpo del metodoEsplicite
TestMock nel costruttoreConfigurazione del LocatorMock nel costruttore
Complessità di configurazioneRichiede contenitore DIRegistro globaleCreazione manuale
Overhead runtimeDagger — compilazioneRicerca a runtimeNessuno

DI vs iniezione manuale — senza un contenitore DI, le dipendenze vengono create manualmente in factory o in AppDelegate. Per 5-10 classi, l'iniezione manuale è più semplice — non richiede l'apprendimento di Dagger o Swinject. Per 50+ classi, l'iniezione manuale diventa problematica: costruttori con 5-6 parametri, ordine di creazione complesso, duplicazione del codice. Un contenitore DI automatizza questi processi e fornisce una chiara gestione del ciclo di vita. L'iniezione manuale senza contenitore è una buona scelta per progetti piccoli e prototipi.

Buone pratiche di Dependency Injection

Iniezione tramite costruttore — standard. Utilizzare sempre l'iniezione tramite costruttore per le dipendenze obbligatorie. Questo rende le dipendenze esplicite e l'oggetto sempre pronto all'uso. Iniezione tramite setter — solo per dipendenze opzionali (es. delegate o listener). Iniezione tramite interfaccia — non usarla a meno che non si stia scrivendo una propria libreria DI. L'iniezione tramite costruttore è l'unico modo per garantire che un oggetto venga creato in uno stato valido.

Una classe — una responsabilità. Se il costruttore di una classe richiede 5+ parametri, probabilmente la classe viola il Principio di Responsabilità Singola. Dividere la classe in più classi con meno dipendenze. Un segnale: se si scrive una classe ServiceManager con 6 servizi diversi — questo è l'anti-pattern God Object. Estrarre la logica di business in Use Cases (Interactors), ciascuno con 1-2 dipendenze.

kotlin
// ❌ Male: 6 dipendenze — 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
)

// ✅ Bene: Use Cases con 1-2 dipendenze
class ProfileViewModel @Inject constructor(
    private val loadProfileUseCase: LoadProfileUseCase,
    private val trackAnalyticsUseCase: TrackAnalyticsUseCase
)

Ambito e ciclo di vita — scegliere l'ambito corretto per ogni dipendenza. Singleton: OkHttpClient, database, SharedPreferences. Ambito di funzionalità: repository, Use Cases (se senza stato). Transient: Value Objects, DateFormatter, parser. Gli errori di ambito sono un problema comune: un singleton che memorizza lo stato dello schermo causa perdite di memoria. Su Android, @ActivityScoped di Hilt risolve questo problema; in Swinject, usare .container con cautela.

Domande frequenti

Perché ho bisogno della DI se posso semplicemente usare new?

new crea un forte accoppiamento tra le classi — non è possibile cambiare implementazione senza modificare il codice. Il test diventa difficile: non è possibile iniettare un mock al posto di un servizio reale. Il SRP viene violato: la classe è responsabile sia della logica di business che della creazione delle dipendenze. La DI risolve questi problemi iniettando le dipendenze dall'esterno e lavorando con astrazioni.

Dagger Hilt o Koin — cosa scegliere per Android?

Dagger Hilt è lo standard di Google, DI in fase di compilazione con generazione di codice, migliori prestazioni e integrazione con Jetpack. Koin è DI in fase di esecuzione, più facile da configurare, ma più lento e con errori a runtime. Scegliere Hilt per progetti di produzione. Koin è adatto per prototipi e applicazioni di piccole dimensioni.

Swinject è l'unica opzione DI per iOS?

No. Per iOS sono disponibili: Swinject (runtime, popolare), Needle (compilazione da Uber), Dip (leggero), Weaver (basato su Sourcery). Apple non fornisce un contenitore DI integrato, ma l'iniezione manuale tramite init è una pratica standard. Per SwiftUI, la DI manuale tramite Environment o @StateObject senza librerie esterne è spesso sufficiente.

Si può usare la DI senza un framework?

Sì. L'iniezione manuale tramite costruttore è DI senza framework. Service Locator è un'alternativa senza framework. Factory e Factory Method sono anch'esse forme di DI. Un framework (Dagger, Swinject) automatizza la registrazione di routine e la risoluzione delle dipendenze, ma per 10-20 classi, la DI manuale è sufficiente.

La DI è un pattern o un principio?

La DI è una tecnica (template) che implementa il principio di Inversione del Controllo. A differenza dei pattern GoF, la DI non ha una struttura rigida di 3-4 classi. La DI è un modo di organizzare le dipendenze, non un pattern di progettazione. I contenitori DI (Dagger, Swinject) sono framework che automatizzano questa tecnica.

Riepilogo

  • DI — una tecnica di iniezione delle dipendenze dall'esterno tramite costruttore, setter o metodo
  • Dagger Hilt — lo standard DI per Android con generazione di codice in fase di compilazione e @HiltViewModel
  • Swinject — DI runtime per iOS con Container, Assembly e ambiti
  • Iniezione tramite costruttore — il metodo preferito per dipendenze obbligatorie
  • Ambito — Singleton per servizi senza stato, ambito di funzionalità per dipendenze a livello di schermata
  • Test — la DI semplifica la sostituzione delle dipendenze con mock senza modificare il codice

Svilupperemo un'applicazione mobile chiavi in mano

IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.

Discuti il progetto

Leggi anche