Android Dependency Injection: Hilt vs Koin – Guida Completa e Domande da Colloquio 2026

Confronto completo tra Hilt e Koin per la Dependency Injection su Android con esempi di codice, benchmark e domande frequenti nei colloqui tecnici 2026.

Android Dependency Injection: Confronto Hilt vs Koin

La Dependency Injection (DI) rappresenta uno dei principi architetturali fondamentali nello sviluppo Android. Invece di creare le dipendenze direttamente all'interno di una classe, queste vengono fornite dall'esterno, migliorando testabilità, manutenibilità e modularità del codice. Nel 2026, due framework dominano questo ambito: Hilt alla versione 2.57.1, la soluzione ufficiale di Google basata su Dagger con validazione a compile-time, e Koin alla versione 4.2.1, un service locator leggero che utilizza una DSL Kotlin pura per la risoluzione a runtime. La scelta tra i due influenza tempi di build, performance all'avvio, testabilità e velocità di onboarding dei nuovi membri del team.

Compile-Time vs Runtime DI

Hilt valida l'intero grafo delle dipendenze durante la compilazione e blocca il build se manca un binding. Koin risolve le dipendenze a runtime attraverso una DSL Kotlin, intercettando gli errori di configurazione solo quando il percorso di codice interessato viene eseguito. Questa differenza fondamentale determina ogni compromesso tra i due framework.

Come funziona Hilt sotto il cofano

Hilt genera componenti Dagger mappati sulle classi del lifecycle Android. L'annotazione @HiltAndroidApp attiva la generazione di codice a compile-time, producendo factory e provider per ogni binding dichiarato. KSP (Kotlin Symbol Processing) ha sostituito KAPT come processore di annotazioni raccomandato a partire da Hilt 2.48, dimezzando approssimativamente i tempi di elaborazione.

Un setup Hilt tipico coinvolge tre elementi: un modulo che dichiara i binding, un entry point (Activity, Fragment o ViewModel) e la classe Application annotata con @HiltAndroidApp.

AppModule.ktkotlin
@Module
@InstallIn(SingletonComponent::class)
object AppModule {

    @Provides
    @Singleton
    fun provideRetrofit(): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://api.example.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    @Provides
    @Singleton
    fun provideUserRepository(
        retrofit: Retrofit
    ): UserRepository {
        return UserRepositoryImpl(retrofit.create(UserApi::class.java))
    }
}
UserViewModel.ktkotlin
@HiltViewModel
class UserViewModel @Inject constructor(
    private val userRepository: UserRepository
) : ViewModel() {

    private val _users = MutableStateFlow<List<User>>(emptyList())
    val users: StateFlow<List<User>> = _users.asStateFlow()

    fun loadUsers() {
        viewModelScope.launch {
            _users.value = userRepository.getUsers()
        }
    }
}

Il compilatore verifica che esista un provider valido per UserRepository prima che l'app venga mai eseguita. I binding mancanti emergono come errori di build, non come crash a runtime.

Setup di Koin e l'approccio DSL Kotlin

Koin adotta l'approccio opposto: zero generazione di codice, zero elaborazione di annotazioni. Le dipendenze vengono dichiarate in Kotlin puro usando una DSL, e il framework le risolve a runtime attraverso un registro di servizi globale. Koin 4.2 ha introdotto i Lazy Modules per il caricamento parallelo all'avvio e un nuovo motore CoreResolverV2 che ottimizza la risoluzione degli scope.

AppModule.ktkotlin
val appModule = module {
    // Istanza Singleton di Retrofit
    single<Retrofit> {
        Retrofit.Builder()
            .baseUrl("https://api.example.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    // Repository singleton legato alla sua interfaccia
    single<UserRepository> {
        UserRepositoryImpl(get<Retrofit>().create(UserApi::class.java))
    }

    // ViewModel con repository iniettato
    viewModel { UserViewModel(get()) }
}
MyApplication.ktkotlin
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        startKoin {
            androidContext(this@MyApplication)
            modules(appModule)
        }
    }
}

La DSL si legge in modo naturale e non richiede alcun plugin di annotation processing nella configurazione Gradle. Il compromesso: un errore di battitura nella risoluzione get() si manifesta solo a runtime quando quel punto di iniezione specifico viene eseguito.

Benchmark delle performance: tempo di build vs costo a runtime

La questione delle performance emerge in quasi ogni colloquio Android. I numeri concreti contano più delle opinioni.

| Metrica | Hilt 2.57 (KSP) | Koin 4.2 | |---------|-----------------|----------| | Overhead clean build | +8-15s (generazione codice KSP) | ~0s (nessuna generazione) | | Overhead build incrementale | +2-4s | ~0s | | Avvio app (50 binding) | ~0ms (codice pre-generato) | ~5-15ms (risoluzione grafo) | | Avvio app (500 binding) | ~0ms | ~30-80ms | | Impatto dimensione APK | +200-400 KB (codice generato) | +100 KB (libreria runtime) | | Rilevamento binding mancanti | Errore di compilazione | Crash a runtime |

Per progetti con meno di 100 binding, l'overhead a runtime di Koin resta impercettibile. Oltre i 300 binding, il costo all'avvio diventa misurabile, sebbene i Lazy Modules di Koin 4.2 mitighino questo aspetto parallelizzando il caricamento dei moduli.

KSP vs KAPT

Passare da KAPT a KSP per l'elaborazione Hilt riduce i tempi di annotation processing del 40-60%. Qualsiasi progetto che utilizza ancora KAPT con Hilt dovrebbe migrare immediatamente a KSP. Hilt supporta KSP dalla versione 2.48.

Strategie di testing con ciascun framework

La testabilità è l'ambito in cui le differenze architetturali diventano concrete. Hilt fornisce @TestInstallIn per sostituire i moduli di produzione con test double a compile-time. Koin offre loadKoinModules() per sovrascrivere le definizioni a runtime.

kotlin
// Modulo di test Hilt — sostituisce i binding di AppModule nei test
@Module
@TestInstallIn(
    components = [SingletonComponent::class],
    replaces = [AppModule::class]
)
object FakeAppModule {

    @Provides
    @Singleton
    fun provideUserRepository(): UserRepository {
        return FakeUserRepository() // Test double in memoria
    }
}
kotlin
// Test Koin — sovrascrittura a runtime
class UserViewModelTest : KoinTest {

    @Before
    fun setUp() {
        startKoin {
            modules(
                module {
                    single<UserRepository> { FakeUserRepository() }
                    viewModel { UserViewModel(get()) }
                }
            )
        }
    }

    @Test
    fun `loads users from repository`() {
        val viewModel: UserViewModel = get()
        viewModel.loadUsers()
        assertEquals(3, viewModel.users.value.size)
    }

    @After
    fun tearDown() {
        stopKoin()
    }
}

I test con Hilt intercettano moduli di test mal configurati a compile-time. I test con Koin richiedono una gestione attenta del lifecycle (startKoin/stopKoin), ma offrono maggiore flessibilità per sovrascritture parziali dei moduli.

Pronto a superare i tuoi colloqui su Android?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

Architettura multi-modulo: scalare la DI attraverso le feature

I grandi progetti Android suddividono le feature in moduli Gradle separati. Il framework DI deve supportare i confini tra moduli in modo pulito.

Con Hilt, ogni feature module dichiara il proprio @Module annotato con @InstallIn. Hilt unifica tutti i moduli in una singola gerarchia di componenti a compile-time. L'architettura MVVM si integra naturalmente con i componenti con scope di Hilt.

feature-payments/PaymentModule.ktkotlin
@Module
@InstallIn(ViewModelComponent::class)
object PaymentModule {

    @Provides
    fun providePaymentGateway(
        retrofit: Retrofit // Fornito dal modulo :app
    ): PaymentGateway {
        return StripePaymentGateway(retrofit.create(PaymentApi::class.java))
    }
}

Con Koin, i feature module esportano le loro dichiarazioni module {}, e il modulo app le carica tutte all'avvio. I Lazy Modules di Koin 4.2 consentono il caricamento differito dei feature module fino al primo accesso.

feature-payments/paymentModule.ktkotlin
val paymentModule = module {
    factory<PaymentGateway> {
        StripePaymentGateway(get<Retrofit>().create(PaymentApi::class.java))
    }
}

// app/MyApplication.kt — carica tutti i feature module
startKoin {
    modules(appModule, paymentModule, analyticsModule)
}

Hilt impone la visibilità delle dipendenze attraverso gli scope dei componenti. Koin si affida alle convenzioni — qualsiasi modulo può accedere alle definizioni di qualsiasi altro modulo, il che offre flessibilità ma può portare a un accoppiamento implicito.

Considerazioni su Kotlin Multiplatform

Koin supporta Kotlin Multiplatform (KMP) nativamente. La stessa DSL module {} funziona su Android, iOS, Desktop e Web. Hilt è esclusivamente per Android — dipende da componenti lifecycle specifici di Android e dal processore di annotazioni Dagger.

Per i progetti che mirano a più piattaforme, Koin è l'unica opzione praticabile tra i due. I moduli di business logic condivisi possono dichiarare le proprie dipendenze una sola volta e iniettarle su tutti i target.

Domande frequenti nei colloqui tecnici sulla DI Android

Queste domande emergono regolarmente nei colloqui tecnici Android. Ogni risposta resta concisa e focalizzata su ciò che gli intervistatori si aspettano.

D: Qual è la differenza tra dependency injection a compile-time e a runtime?

La DI a compile-time (Hilt/Dagger) genera il codice di iniezione durante il build. Il compilatore valida l'intero grafo delle dipendenze, intercettando i binding mancanti prima del runtime. La DI a runtime (Koin) risolve le dipendenze quando vengono richieste per la prima volta, usando un pattern service registry. La DI a compile-time produce un avvio più rapido ma build più lenti; la DI a runtime non ha overhead sul build ma rinvia il rilevamento degli errori.

D: Quali scope dei componenti Hilt esistono e come si mappano ai lifecycle Android?

SingletonComponent vive per l'intera durata dell'applicazione. ActivityRetainedComponent sopravvive ai cambiamenti di configurazione. ViewModelComponent è legato al lifecycle di un ViewModel. ActivityComponent, FragmentComponent e ViewComponent seguono i rispettivi lifecycle owner Android. Scope personalizzati possono estendere questa gerarchia.

D: Come gestisce Koin l'iniezione dei ViewModel in Jetpack Compose?

Koin fornisce koinViewModel() come funzione Composable che crea o recupera un ViewModel con scope al ViewModelStoreOwner più vicino. Da Koin 4.2, koinNavViewModel() vincola i ViewModel alle voci del navigation graph quando si utilizza Jetpack Compose Navigation.

D: Quando Koin sarebbe una scelta migliore rispetto a Hilt?

Koin si adatta meglio ai progetti KMP (Hilt è solo per Android), alle app di piccole e medie dimensioni dove il tempo di build conta più del tempo di avvio, e ai team che preferiscono una DSL Kotlin esplicita rispetto alla configurazione basata su annotazioni. Anche i progetti di prototyping e proof-of-concept beneficiano del setup minimale di Koin.

Errore comune nei colloqui

Definire Koin come un "framework di dependency injection" è tecnicamente impreciso. Koin è un service locator — le dipendenze vengono estratte tramite get() anziché iniettate tramite constructor injection. Gli intervistatori che conoscono questa distinzione si aspettano che i candidati la riconoscano. Hilt/Dagger esegue una vera dependency injection attraverso chiamate al costruttore generate.

Schema decisionale: scegliere tra Hilt e Koin

| Fattore | Hilt | Koin | |---------|------|------| | Validazione del grafo | Compile-time | Runtime | | Impatto sul tempo di build | Maggiore (codegen KSP) | Nessuno | | Costo di avvio | Quasi zero | Scala con la dimensione del grafo | | Curva di apprendimento | Più ripida (concetti Dagger) | Dolce (pura DSL Kotlin) | | Supporto KMP | No | Sì | | Integrazione Jetpack | Profonda (ufficiale Google) | Buona (community) | | Scalabilità del team | Più forte (scope imposti) | Flessibile (basato su convenzioni) | | Testing | @TestInstallIn (compile-safe) | loadKoinModules (runtime) |

Nessuno dei due framework è universalmente superiore. La scelta dipende dalla dimensione del progetto, dall'esperienza del team e dalle piattaforme target.

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

Conclusione

  • Hilt 2.57 con KSP offre validazione del grafo a compile-time e costo di avvio quasi nullo, rendendolo la scelta più solida per grandi progetti Android single-platform con requisiti di affidabilità rigorosi
  • Koin 4.2 con Lazy Modules e CoreResolverV2 colma il divario nelle performance di avvio mantenendo zero overhead di build e piena compatibilità KMP
  • Entrambi i framework gestiscono architetture multi-modulo, ma Hilt impone i confini degli scope a compile-time mentre Koin si affida alle convenzioni del team
  • Per la preparazione ai colloqui Android, comprendere il compromesso compile-time vs runtime e saper articolare quando ciascun framework è più adatto rappresenta la base attesa
  • Migrare tra i due framework non è banale — la scelta iniziale dovrebbe tenere conto della traiettoria di piattaforma e scalabilità a lungo termine del progetto

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

Tag

#android
#kotlin
#dependency-injection
#hilt
#koin

Condividi

Articoli correlati