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 è 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 DI | Metodo | Quando usarlo | Esempio |
|---|---|---|---|
| Costruttore | Parametri dell'inizializzatore | Dipendenze obbligatorie | init(service: ServiceProtocol) |
| Proprietà | Proprietà della classe | Dipendenze opzionali | var service: ServiceProtocol? |
| Metodo | Parametro del metodo | Dipendenze temporanee | func 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 è 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.
// 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 è 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.
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.
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.
| Caratteristica | Iniezione delle dipendenze | Service Locator | Iniezione manuale |
|---|---|---|---|
| Visibilità delle dipendenze | Nel costruttore | Nascoste nel corpo del metodo | Esplicite |
| Test | Mock nel costruttore | Configurazione del Locator | Mock nel costruttore |
| Complessità di configurazione | Richiede contenitore DI | Registro globale | Creazione manuale |
| Overhead runtime | Dagger — compilazione | Ricerca a runtime | Nessuno |
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.
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.
// ❌ 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
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 è 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.
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.
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 è 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
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.
Leggi anche