docs/reference/koin-android/hilt-migration.md
This guide helps you migrate your Android application from Dagger Hilt to Koin. Whether you're using Koin DSL or Koin Annotations, this guide covers the key differences and migration steps.
:::info For a complete real-world example, check out the Now in Android migration which shows how Google's production-ready news app with 30 Gradle modules was migrated from Hilt to Koin Annotations. :::
Key advantages of Koin:
@InstallIn declarations@EntryPoint interfaces needed@Inject constructors work without modification| Hilt | Koin DSL | Koin Annotations |
|---|---|---|
@HiltAndroidApp | startKoin {} in Application | @KoinApplication |
@AndroidEntryPoint | by inject() / by viewModel() | by inject() / by viewModel() |
@HiltViewModel | viewModel { MyViewModel(...) } | @KoinViewModel |
@Inject constructor | DSL to specify constructor parameters | Constructor parameters detected (JSR-330) |
@Module + @InstallIn | module { } | @Module + @ComponentScan |
@Provides | single { } or factory { } | @Single / @Factory |
@Binds | single<Interface> { Implementation() } | @Single or @Singleton are detecting bindings. Also use binds property from those annotations. |
@Singleton | single { } | @Singleor @Singleton |
@Named("qualifier") | named("qualifier") | @Named("qualifier") |
@ApplicationContext | Automatic context injection | Automatic context injection |
@EntryPoint | Not needed | Not needed |
| Hilt Scope | Koin DSL | Koin Annotations | Notes |
|---|---|---|---|
@Singleton | single { } | @Single / @Singleton | Application-wide singleton |
@ActivityScoped | activityScope { scoped { } } | @ActivityScope | Tied to Activity lifecycle |
@ViewModelScoped | viewModelScope { scoped { } } | @ViewModelScope | Tied to ViewModel lifecycle |
@ActivityRetainedScoped | activityRetainedScope { scoped { } } | @ActivityRetainedScope | Survives configuration changes |
Remove Hilt dependencies:
// Remove these from build.gradle.kts
plugins {
id("com.google.dagger.hilt.android") // Remove
}
dependencies {
// Remove Hilt dependencies
implementation("com.google.dagger:hilt-android:...")
kapt("com.google.dagger:hilt-compiler:...")
}
Add Koin dependencies:
// build.gradle.kts (app module)
dependencies {
// Koin for Android
implementation("io.insert-koin:koin-android:$koin_version")
implementation("io.insert-koin:koin-androidx-compose:$koin_version")
// Optional: Koin Annotations
implementation("io.insert-koin:koin-annotations:$koin_ksp_version")
ksp("io.insert-koin:koin-ksp-compiler:$koin_ksp_version")
}
Hilt:
@HiltAndroidApp
class MyApplication : Application()
Koin DSL:
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidLogger()
androidContext(this@MyApplication)
modules(appModule, dataModule, domainModule)
}
}
}
Koin Annotations:
@KoinApplication
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidLogger()
androidContext(this@MyApplication)
}
}
}
:::info
With @KoinApplication, modules are automatically discovered if they're tagged with @Configuration. You can also explicitly include modules using the modules property: @KoinApplication(modules = [AppModule::class]).
:::
Hilt:
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.build()
}
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.example.com")
.client(okHttpClient)
.build()
}
@Provides
@Singleton
fun provideApiService(retrofit: Retrofit): ApiService {
return retrofit.create(ApiService::class.java)
}
}
Koin DSL:
val networkModule = module {
single {
OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.build()
}
single {
Retrofit.Builder()
.baseUrl("https://api.example.com")
.client(get()) // Automatic dependency resolution
.build()
}
single {
get<Retrofit>().create(ApiService::class.java)
}
}
Koin Annotations:
@Module
class NetworkModule {
@Single
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.build()
}
@Single
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.example.com")
.client(okHttpClient)
.build()
}
@Single
fun provideApiService(retrofit: Retrofit): ApiService {
return retrofit.create(ApiService::class.java)
}
}
Hilt:
@HiltViewModel
class MyViewModel @Inject constructor(
private val repository: MyRepository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
// ...
}
@Composable
fun MyScreen() {
val viewModel = hiltViewModel<MyViewModel>()
// ...
}
Koin DSL:
class MyViewModel(
private val repository: MyRepository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
// ...
}
val appModule = module {
viewModelOf(::MyViewModel)
}
@Composable
fun MyScreen() {
val viewModel = koinViewModel<MyViewModel>()
// ...
}
Koin Annotations:
@KoinViewModel
class MyViewModel(
private val repository: MyRepository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
// ...
}
@Composable
fun MyScreen() {
val viewModel = koinViewModel<MyViewModel>()
// ...
}
:::info
The viewModelOf DSL function uses constructor parameter autowiring. SavedStateHandle is automatically provided by Koin, so you don't need to explicitly pass it. This is part of Koin's autowire DSL which simplifies ViewModel definitions.
:::
Hilt:
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@Inject
lateinit var analytics: AnalyticsService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
analytics.logEvent("screen_view")
}
}
Koin:
class MainActivity : ComponentActivity() {
// Property delegation - no annotation needed
private val analytics: AnalyticsService by inject()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
analytics.logEvent("screen_view")
}
}
:::info
With Koin, you don't need @AndroidEntryPoint - just use by inject() or by viewModel() property delegation.
:::
Hilt:
@Module
@InstallIn(SingletonComponent::class)
abstract class DataModule {
@Binds
@Singleton
abstract fun bindRepository(
impl: MyRepositoryImpl
): MyRepository
}
class MyRepositoryImpl @Inject constructor(
private val apiService: ApiService
) : MyRepository {
// ...
}
Koin DSL:
val dataModule = module {
single<MyRepository> { MyRepositoryImpl(get()) }
}
class MyRepositoryImpl(
private val apiService: ApiService
) : MyRepository {
// ...
}
Koin Annotations (Automatic Binding Detection):
// Option 1: Automatic - Koin detects the interface binding
@Singleton
class MyRepositoryImpl(
private val apiService: ApiService
) : MyRepository {
// ...
}
// Koin automatically binds MyRepositoryImpl to MyRepository
// Option 2: Explicit with binds property
@Single(binds = [MyRepository::class])
class MyRepositoryImpl(
private val apiService: ApiService
) : MyRepository {
// ...
}
:::info
Koin Annotations automatically detects interface bindings when a class implements an interface. Use the binds property when you need to explicitly specify multiple interfaces or control binding behavior.
:::
Hilt:
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class IoDispatcher
@Module
@InstallIn(SingletonComponent::class)
object DispatcherModule {
@Provides
@IoDispatcher
fun provideIoDispatcher(): CoroutineDispatcher {
return Dispatchers.IO
}
}
class MyRepository @Inject constructor(
@IoDispatcher private val dispatcher: CoroutineDispatcher
)
Koin DSL (String-based):
val dispatcherModule = module {
single(named("io")) { Dispatchers.IO }
}
class MyRepository(
private val dispatcher: CoroutineDispatcher
)
val dataModule = module {
single { MyRepository(get(named("io"))) }
}
Koin DSL (Type-safe):
// Define a qualifier type
object IoDispatcher
val dispatcherModule = module {
single(named<IoDispatcher>()) { Dispatchers.IO }
}
class MyRepository(
private val dispatcher: CoroutineDispatcher
)
val dataModule = module {
single { MyRepository(get(named<IoDispatcher>())) }
}
Koin Annotations (String-based):
@Module
class DispatcherModule {
@Single
@Named("io")
fun provideIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
}
@Single
class MyRepository(
@InjectedParam @Named("io") private val dispatcher: CoroutineDispatcher
)
Koin Annotations (With JSR-330 @Qualifier - Fully Compatible!):
// Keep your existing JSR-330 qualifier annotation!
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class IoDispatcher
@Module
class DispatcherModule {
@Single
@IoDispatcher
fun provideIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
}
@Single
class MyRepository @Inject constructor(
@IoDispatcher private val dispatcher: CoroutineDispatcher
)
:::info
Koin Annotations fully supports JSR-330 @Qualifier annotations! This is a standard Java/Kotlin DI annotation (not Hilt-specific), so you can keep your existing qualifier annotations unchanged during migration. The DSL also supports type-safe qualifiers using named<T>() instead of string-based named("string").
:::
Hilt:
@Composable
fun MyScreen(
viewModel: MyViewModel = hiltViewModel()
) {
val dependency: SomeDependency = EntryPointAccessors
.fromActivity<MyEntryPoint>(LocalContext.current as Activity)
.dependency()
}
Koin:
@Composable
fun MyScreen(
viewModel: MyViewModel = koinViewModel()
) {
// Direct injection - no EntryPoint needed
val dependency: SomeDependency = koinInject()
}
Hilt:
@HiltAndroidTest
class MyTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
@Inject
lateinit var repository: MyRepository
@Before
fun init() {
hiltRule.inject()
}
@Test
fun myTest() {
// ...
}
}
Koin:
class MyTest : KoinTest {
private val repository: MyRepository by inject()
@Before
fun before() {
startKoin {
modules(testModule)
}
}
@After
fun after() {
stopKoin()
}
@Test
fun myTest() {
// ...
}
}
With Hilt, you need:
@InstallIn to specify component hierarchy@EntryPoint interfaces for cross-module accessWith Koin:
Feature module with Koin:
// :feature:home module
val homeModule = module {
viewModel { HomeViewModel(get()) }
factory { HomeRepository(get()) }
}
// :app module
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidLogger()
androidContext(this@MyApplication)
modules(
coreModule,
dataModule,
homeModule, // Feature module
profileModule // Another feature module
)
}
}
}
See Multi-Module Architecture for more details.
One of the biggest advantages: existing @Inject constructors work with Koin Annotations!
// This works with both Hilt and Koin Annotations
class MyRepository @Inject constructor(
private val apiService: ApiService,
private val database: AppDatabase
) {
// ...
}
With Koin Annotations, you can keep your @Inject constructors unchanged and just add @Single, @Singleton, or @Factory to the class:
@Single // or @Singleton
class MyRepository @Inject constructor(
private val apiService: ApiService,
private val database: AppDatabase
) {
// ...
}
Hilt:
class MyViewModel @AssistedInject constructor(
private val repository: MyRepository,
@Assisted private val userId: String
) : ViewModel() {
@AssistedFactory
interface Factory {
fun create(userId: String): MyViewModel
}
}
Koin:
class MyViewModel(
private val repository: MyRepository,
private val userId: String
) : ViewModel()
val appModule = module {
viewModelOf(::MyViewModel)
}
// Usage
val viewModel: MyViewModel by viewModel { parametersOf("user123") }
Hilt:
@Inject
lateinit var heavyService: HeavyService
Koin:
// Lazy by default with property delegation
private val heavyService: HeavyService by inject()
// Or explicit lazy
private val heavyService: Lazy<HeavyService> by lazy { get() }
Use this checklist to track your migration progress:
Dependencies
kapt if not needed elsewhereApplication Class
@HiltAndroidAppstartKoin {} in onCreate()androidContext() and modulesModules
@Module + @InstallIn to module { }@Provides to single { } or factory { }@Binds to interface bindingsnamed()ViewModels
@HiltViewModelviewModel { }koinViewModel()Activities/Fragments
@AndroidEntryPointby inject()Testing
@HiltAndroidTestKoinTeststartKoin / stopKoin in setup/teardownVerification
Issue: Koin can't find a definition for a type.
Solution:
startKoin { modules(...) }single { } or factory { })Issue: Multiple definitions for the same type.
Solution:
single(named("qualifier")) { }startKoin { allowOverride(true) }Issue: Two classes depend on each other.
Solution:
lazy injection: private val service by lazy { get<MyService>() }koin