Skip to main content
Version: 4.2

Migrating from Hilt

This guide moves an Android app from Dagger Hilt to Koin, one step at a time: Gradle, the Application class, modules, bindings, ViewModels, entry points, qualifiers, Compose and tests. Each Koin sample comes in the three Koin styles; if you are not sure which one to pick, read Choosing your style. For a comparison of the two frameworks, see Koin vs Hilt.

note

For a full real-world migration, read Migrating Now in Android to Koin: Google's sample app with 30 Gradle modules, moved from Hilt to Koin Annotations.

Concept mapping​

HiltCompiler Plugin DSLAnnotationsClassic DSL
@HiltAndroidAppstartKoin { } in Application.onCreate()@KoinApplication + startKoin<T>()startKoin { } in Application.onCreate()
@AndroidEntryPointNot needed: by inject(), by viewModel()Not neededNot needed
@Inject constructorsingle<T>() / factory<T>()@Singleton / @Factory on the classsingleOf(::T) / factoryOf(::T)
@Module + @InstallInmodule { }@Module + @ComponentScanmodule { }
@Providessingle { create(::fn) }@Singleton / @Factory on a functionsingle { ... }
@Bindsbind Interface::classAutomatic (supertypes) or binds = [...]bind Interface::class
@Singletonsingle@Singleton / @Singlesingle
@HiltViewModelviewModel<T>()@KoinViewModelviewModelOf(::T)
@HiltWorkerworker<T>()@KoinWorkerworkerOf(::T)
@Named, custom @Qualifiernamed("x"), @Named on parameters@Named, custom @Qualifiernamed("x")
@ApplicationContextContext parameter, provided by androidContext()SameandroidContext()
@AssistedInjectInjected parameters@InjectedParamInjected parameters
@Inject field in a class you ownConstructor parameterConstructor parameterConstructor parameter
@EntryPoint (framework class Hilt can't inject)KoinComponent at that entry point onlySameSame
@HiltAndroidTestKoinTestRuleKoinTestRuleKoinTestRule

Scope mapping​

HiltKoin DSLAnnotationsLifetime
@Singleton (SingletonComponent)single@SingletonApplication
@ActivityRetainedScopedactivityRetainedScope { }@ActivityRetainedScopeSurvives configuration changes
@ViewModelScopedviewModelScope { }@ViewModelScopeViewModel
@ActivityScopedactivityScope { }@ActivityScopeActivity
@FragmentScopedfragmentScope { }@FragmentScopeFragment

Scope DSL and annotations are explained in Android Scopes.

Step 1: Gradle​

Remove the Hilt plugin, hilt-android, and hilt-compiler (kapt or KSP). Then add Koin:

dependencies {
implementation(platform("io.insert-koin:koin-bom:$koin_version"))
implementation("io.insert-koin:koin-android")
implementation("io.insert-koin:koin-androidx-compose") // if you use Compose
}

To use the Compiler Plugin DSL or annotations, also apply the Koin Compiler Plugin. It runs inside the Kotlin compiler and replaces kapt or KSP for Koin. The full list of artifacts is in Gradle Setup.

Step 2: Application class​

Hilt:

@HiltAndroidApp
class MyApplication : Application()

Koin:

class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidLogger()
androidContext(this@MyApplication)
modules(appModule)
}
}
}

androidContext() makes the Context available to every definition. See Starting Koin on Android.

Step 3: Modules and @Provides​

Hilt:

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient = OkHttpClient.Builder().build()

@Provides
@Singleton
fun provideApiService(client: OkHttpClient): ApiService =
Retrofit.Builder()
.baseUrl("https://api.example.com")
.client(client)
.build()
.create(ApiService::class.java)
}

Koin:

import org.koin.plugin.module.dsl.create

fun okHttpClient(): OkHttpClient = OkHttpClient.Builder().build()

fun apiService(client: OkHttpClient): ApiService =
Retrofit.Builder()
.baseUrl("https://api.example.com")
.client(client)
.build()
.create(ApiService::class.java)

val networkModule = module {
single { create(::okHttpClient) }
single { create(::apiService) }
}

There is no @InstallIn: a definition's lifetime comes from its type (single, factory, scoped), not from the component it is installed in. Classes with an @Inject constructor become single<T>() / factory<T>(), an annotation on the class, or singleOf(::T). More on library objects in Definitions.

Step 4: Interface bindings​

Hilt:

@Module
@InstallIn(SingletonComponent::class)
abstract class DataModule {
@Binds
@Singleton
abstract fun bindUserRepository(impl: UserRepositoryImpl): UserRepository
}

Koin:

val dataModule = module {
single<UserRepositoryImpl>() bind UserRepository::class
}

Step 5: ViewModels​

Hilt:

@HiltViewModel
class UserViewModel @Inject constructor(
private val repository: UserRepository,
private val savedStateHandle: SavedStateHandle,
) : ViewModel()

Koin:

class UserViewModel(
private val repository: UserRepository,
private val savedStateHandle: SavedStateHandle,
) : ViewModel()

val appModule = module {
viewModel<UserViewModel>()
}

Koin provides SavedStateHandle to ViewModel definitions. In an Activity or Fragment, replace by viewModels() with by viewModel(). In Compose, replace hiltViewModel() with koinViewModel(). See Android ViewModel.

Step 6: Activities, Fragments and other entry points​

Hilt:

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@Inject lateinit var analytics: Analytics
}

Koin:

class MainActivity : ComponentActivity() {
private val analytics: Analytics by inject()
}

Remove @AndroidEntryPoint. In Activities, Fragments and other Android components, which the framework creates, fields with @Inject lateinit var become by inject() (lazy) or get() (immediate). This is the same in every style. Services, BroadcastReceivers and other components are covered in Entry Points.

In your own classes (repositories, services, use cases, ViewModels), move @Inject fields to constructor parameters and declare the class as in Step 3. These classes never call get() or by inject():

// Hilt: field injection in a class you own
class SyncService @Inject constructor() {
@Inject lateinit var repository: UserRepository
}

// Koin: constructor injection
class SyncService(private val repository: UserRepository)

Hilt's @EntryPoint exists for framework classes Hilt can't inject, such as a ContentProvider or a library callback. Only at such an entry point, implement KoinComponent and call by inject(). Do not use KoinComponent to replace @Inject fields in app classes.

Step 7: Qualifiers​

Hilt:

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class IoDispatcher

@Provides
@IoDispatcher
fun provideIoDispatcher(): CoroutineDispatcher = Dispatchers.IO

class SyncRepository @Inject constructor(
@IoDispatcher private val dispatcher: CoroutineDispatcher,
)

Koin:

import org.koin.core.annotation.Named

class SyncRepository(
@Named("io") private val dispatcher: CoroutineDispatcher,
)

val dispatcherModule = module {
single<CoroutineDispatcher>(named("io")) { Dispatchers.IO }
single<SyncRepository>()
}

Type qualifiers (named<IoDispatcher>()) and all other options are in Qualifiers. Hilt's @ApplicationContext needs no qualifier: a Context parameter receives the context passed to androidContext().

Step 8: Compose​

Hilt needs an entry point to reach a non-ViewModel dependency from a Composable. Koin resolves it directly:

@Composable
fun UserScreen(viewModel: UserViewModel = koinViewModel()) {
val analytics: Analytics = koinInject()
}

See Koin for Compose.

Step 9: Tests​

Hilt:

@HiltAndroidTest
class UserRepositoryTest {
@get:Rule
val hiltRule = HiltAndroidRule(this)

@Inject lateinit var repository: UserRepository

@Before
fun init() = hiltRule.inject()
}

Koin:

class UserRepositoryTest : KoinTest {

@get:Rule
val koinTestRule = KoinTestRule.create {
modules(appModule, testModule)
}

private val repository: UserRepository by inject()
}

Hilt's @TestInstallIn and @UninstallModules become a test module loaded after the production modules. Remove HiltTestApplication and the Hilt test runner. See Unit Testing and Android Instrumented Testing.

Migrate gradually with @Inject​

With annotations, the Compiler Plugin reads JSR-330 annotations: in a package covered by @ComponentScan, a class with an @Inject constructor becomes a factory, and a class annotated with @Singleton (from jakarta.inject or javax.inject) a single. You can keep these classes unchanged and migrate Gradle module by Gradle module. See JSR-330 Annotations.

Multi-module projects​

Each Gradle module declares its own Koin module, and the app module loads them all in startKoin. There is no component hierarchy to configure and no @EntryPoint for cross-module access: a definition in :feature:home can use any definition loaded by the app.

With annotations, tag each feature module class with @Module, @ComponentScan and @Configuration. @KoinApplication in the app module finds them without listing them.

Module organization, visibility and feature modules are covered in Multi-Module Apps.

Common patterns​

Assisted injection​

Hilt's @AssistedInject and @AssistedFactory become injected parameters. The caller passes the runtime value:

class UserDetailViewModel(
@InjectedParam private val userId: String,
private val repository: UserRepository,
) : ViewModel()

val appModule = module {
viewModel<UserDetailViewModel>()
}
val viewModel: UserDetailViewModel by viewModel { parametersOf("user-42") }

See Injected Parameters.

Lazy and Provider​

  • dagger.Lazy<T>: use a Kotlin Lazy<T> constructor parameter, or by inject() in an Android entry point (see Injection).
  • Provider<T>: to create a new instance on each call from a class you own, use the provider pattern in Advanced Patterns.

WorkManager​

@HiltWorker workers become worker definitions, and HiltWorkerFactory becomes workManagerFactory() in startKoin. See WorkManager.

Checklist​

  • Gradle: remove the Hilt plugin, hilt-android and hilt-compiler. Add Koin, and the Compiler Plugin if you use it. Remove kapt if nothing else needs it.
  • Application: remove @HiltAndroidApp, start Koin in onCreate() with androidContext().
  • Modules: convert @Module / @InstallIn / @Provides / @Binds to Koin modules.
  • ViewModels: remove @HiltViewModel, declare them, use by viewModel() / koinViewModel().
  • Entry points: remove @AndroidEntryPoint, use by inject() in Android components. Move @Inject fields of your own classes to constructor parameters. Replace @EntryPoint with KoinComponent only in framework classes Koin can't create.
  • Qualifiers: keep custom @Qualifier annotations (annotations style) or switch to named().
  • Workers: replace @HiltWorker and HiltWorkerFactory.
  • Tests: remove @HiltAndroidTest, HiltAndroidRule and the Hilt test runner, use KoinTestRule.
  • Build: with the Compiler Plugin, a successful build means the graph is complete. Without it, add a verify() test.
  • Run: launch the app and the test suite.

Troubleshooting​

NoDefinitionFoundException​

Koin has no definition for the requested type. Check that the module declaring it is loaded in startKoin (or found by @Configuration), and that the requested type matches the declared or bound type. A class that only had @Inject constructor and was injected by Hilt needs a Koin definition too, unless a @ComponentScan picks it up through its JSR-330 annotations.

Wrong instance with two definitions of the same type​

With Hilt, a missing qualifier is a build error. With Koin, the last loaded definition wins, unless you start Koin with allowOverride(false). Add a qualifier to both definitions and to the injection points.

Circular dependency​

Two classes depend on each other. Refactor to break the cycle, or make one side lazy with a Lazy<T> parameter. The Compiler Plugin reports cycles at build time. More in Troubleshooting.