From Hilt to Koin using Koin Annotations

Santiago Mattiauda
ProAndroidDev
Published in
9 min readJul 1, 2024

Dependency Injection (DI) is an essential technique in Android application development for managing dependencies efficiently and modularly. Hilt and Koin are two popular DI frameworks in Android. Hilt, developed by Google, is based on Dagger, while Koin, developed by Arnaud Giuliani, is a lightweight and easy to configure framework. Recently, Koin has introduced annotations, which makes migration from Hilt easier. In this article, we will explore Hilt and Koin, comparing both frameworks, and going through a detailed process for migrating from Hilt to Koin using annotations.

Introduction to Hilt

What is Hilt?

Hilt is a dependency injection framework for Android, developed by Google. It simplifies the integration of Dagger into Android applications and offers a standardized way to inject dependencies into Android components, such as Activities, Fragments, Services, and ViewModels.

Hilt features

  1. Integration with Dagger: Hilt is based on Dagger, which provides the solidity and efficiency of this framework.
  2. Simplicity: Hilt makes Dagger configuration easier by reducing repetitive code.
  3. Integration with AndroidX: It offers native support for ViewModels and WorkManager.
  4. Scopes and Modules: It allows clear management of dependency scopes and modular definition of them.

Hilt configuration example

To use Hilt, we first need to add the necessary dependencies.

We define the following plugins and dependencies in our dependency file (.toml):

[versions]
hiltGradlePlugin = "2.51.1"
ksp = "2.0.0-1.0.22"

[libraries]
hilt_android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
hilt_compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" }
hilt_navigation = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltCompose" }

[plugins]
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hiltGradlePlugin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }

Next, in our project’s build.gradle.kts file, we declare the Hilt plugin and KSP.

plugins {
...
alias(libs.plugins.ksp) apply false
alias(libs.plugins.hilt) apply false
}

Once the plugins are declared, we can apply them to our build.gradle.kts of the app module.

plugins {
...
alias(libs.plugins.ksp)
alias(libs.plugins.hilt)
}

Next, we add the dependencies:

dependencies {
implementation(libs.hilt.android)
//Compose
implementation(libs.hilt.navigation)
ksp(libs.hilt.compiler)
}

Once the project is set up, we can start using Hilt.

Let’s define our first dependency, for example, a class called MyRepository:

class MyRepository @Inject constructor()

Next, we annotate our Application class with @HiltAndroidApp. If we don’t have an Application class, we should define one so that Hilt can use it as a starting point for our dependency graph.

@HiltAndroidApp
class MyApp : Application()

To inject dependencies into an Activity, we must annotate it as @AndroidEntryPoint and mark our property with @Inject, as shown in the following example.

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

@Inject lateinit var repository: MyRepository

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}

So far, we have seen the basic configuration and a simple example of how to use Hilt in an Android project.

For a deeper understanding of Hilt and its implementation, it is recommended to review the official Hilt documentation.

Now let’s see how to do the same with Koin.

Introduction to Koin

What is Koin?

Koin is a dependency injection framework for Kotlin. It is designed to be simple and lightweight. Its configuration is less complex than Dagger’s, which makes it quick to adopt in new and existing projects.

Features of Koin

  1. Simplicity: Koin is easy to set up and use, thanks to its clear and concise syntax.
  2. No annotation processor required: Unlike Dagger/Hilt, Koin does not need an annotation processor. This will become evident when we look at Koin’s annotations, but it is not necessary for basic usage.
  3. Kotlin DSL based configuration: Dependency injection modules are defined using Kotlin DSL.

Example of Koin configuration

To use Koin, first we must add the necessary dependencies in the .toml file:

[versions]
koinVersion = "3.5.6"

[libraries]
koin-android = { module = "io.insert-koin:koin-android", version.ref = "koinVersion" }

We declare these dependencies in our build.gradle.kts file:

dependencies {
implementation(libs.koin.android)
}

Next, we define a Koin module:

val appModule = module {
single { MyRepository() }
}

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

To inject dependencies into an Activity, use by inject().

class MainActivity : AppCompatActivity() {
private val repository: MyRepository by inject()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}

As we can see, Koin is simpler to configure than Hilt. However, let’s see how to migrate a slightly more complex project more directly using Koin Annotations.

Migration from Hilt to Koin using Annotations

The recent version of Koin has introduced support for annotations, thus facilitating migration from Hilt. Below, we will see a step-by-step process to migrate from Hilt to Koin using annotations.

Project Description

We will address a slightly more complex example than those previously seen, where we have the following components.

The first level contains the MainViewModel class, which manages the communication between the user interface and the data layer. We have a repository, MovieRepository, which interacts with two data sources: one local, RoomMovieDataSource, using Room, and another remote, RetrofitMovieDataSource, using Retrofit. Both data sources are decoupled, as they have their respective abstractions, MovieLocalDataSource and MovieRemoteDataSource.

In the example, we use Hilt as a dependency injector. Let’s see its configurations.

To provide the data sources, we have defined the following Hilt module.

@Module
@InstallIn(SingletonComponent::class)
class DataModule {

@Provides
@Singleton
fun provideMovieRemoteDataSource(retrofitServiceCreator: RetrofitServiceCreator): MovieRemoteDataSource {
return RetrofitMovieDataSource(retrofitServiceCreator.create(TheMovieDBService::class))
}

@Provides
@Singleton
fun provideMovieLocalDataSource(appDataBase: AppDataBase): MovieLocalDataSource {
return RoomMovieDataSource(appDataBase)
}
}

A Hilt module is a class annotated with @Module. Just like the Dagger modules, these inform Hilt on how to provide instances of certain types. However, unlike Dagger modules, you must annotate Hilt modules with @InstallIn to indicate to Hilt in which Android class each module will be used or installed.

And our MovieRepository is defined as follows:

@Singleton
class MovieRepository @Inject constructor(
private val remoteDataSource: MovieRemoteDataSource,
private val localDataSource: MovieLocalDataSource,
){
// implementation
}

Then, in our ViewModel, we have defined it as a HiltViewModel so that Hilt can recognize it as such and take into account that it requires an instance of MovieRepository.

@HiltViewModel
class MainViewModel @Inject constructor(
private val repository: MovieRepository
) : ViewModel(){
// implementation
}

Finally, we inject our ViewModel, this time using Jetpack Compose with the hiltViewModel function.

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BasicSkeletonContainer {
MainRoute()
}
}
}
}

@Composable
fun MainRoute(
viewModel: MainViewModel = hiltViewModel(),
) {
val state by viewModel.state.collectAsStateWithLifecycle()
MainScreen(
state = state,
onRefresh = viewModel::refresh,
)
}

As you may observe, I did not go into details of each annotation nor the management of dependencies. This is simply because we are going to focus on the migration.

To see what the Hilt annotations are, I recommend checking the following link:

Step 1: Set Up Koin Annotations

Just like with Hilt at the beginning of this article, we also define the necessary KSP plugin for Koin to generate our code, in addition to the necessary dependencies.

[versions]
ksp = "2.0.0-1.0.22"

koinVersion = "3.5.6"
koinAnnotationVersion = "1.3.1"

[libraries]
koin-android = { module = "io.insert-koin:koin-android", version.ref = "koinVersion" }
koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koinVersion" }
koin-annotations = { module = "io.insert-koin:koin-annotations", version.ref = "koinAnnotationVersion" }
koin-ksp-compiler = { module = "io.insert-koin:koin-ksp-compiler", version.ref = "koinAnnotationVersion" }

[plugins]
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }

To our previous definition of Koin, we add the following dependencies:

  • koin-androidx-compose: for Jetpack Compose
  • koin-annotations: for Koin annotations
  • koin-ksp-compiler: for the Koin annotations compiler.

Include the Koin Annotations dependencies in the build.gradle.kts file:

dependencies {
implementation(libs.koin.android)
implementation(libs.koin.androidx.compose)
implementation(libs.koin.annotations)
ksp(libs.koin.ksp.compiler)
}

In addition to setting up the dependencies, it’s important to keep certain steps in mind for configurations to use Koin Annotations. We need to tell our project to recognize the code generated by Koin. For this, we add the following configuration in our build.gradle.kts file.

android {

applicationVariants.forEach { variant ->
variant.sourceSets.forEach {
it.javaDirectories += files("build/generated/ksp/${variant.name}/kotlin")
}
}

}

With this configuration, we will be able to use the code generated from our project.

Step 2: Define Koin annotations

Replace Hilt annotations with Koin ones. For example:

import org.koin.core.annotation.Module
import org.koin.core.annotation.Single

@Module
class DataModule {

@Single
fun provideMovieRemoteDataSource(
retrofitServiceCreator: RetrofitServiceCreator
): MovieRemoteDataSource {
val service = retrofitServiceCreator.create(TheMovieDBService::class)
return RetrofitMovieDataSource(service)
}

@Single
fun provideMovieLocalDataSource(
appDataBase: AppDataBase
): MovieLocalDataSource {
return RoomMovieDataSource(appDataBase)
}
}

@Single is equivalent to @Singleton in Hilt, even the @Singleton annotation is found in Koin with the same purpose. Now, let’s migrate our repository using @Single.

@Single
class MovieRepository(
private val remoteDataSource: MovieRemoteDataSource,
private val localDataSource: MovieLocalDataSource,
) {
// implementation
}

Now we will do the same with our ViewModel, for this in Koin we have KoinViewModel.

@KoinViewModel
class MainViewModel(
private val repository: MovieRepository
) : ViewModel() {
//implementation
}

Step 3: Initialize Koin in the Application

Set up Koin in the Application class:

import android.app.Application
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin
import org.koin.ksp.generated.defaultModule
import org.koin.ksp.generated.module

class MainApplication : Application(){

override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@MainApplication)
modules(
DataModule().module,
AppModule().module,
defaultModule
)
}
}
}

Let’s analyze the Koin initialization code. First, we generate an instance of the DataModule() class, which we define and annotate as @Module, even though it has no module property. This is where Koin Annotations comes in. Having annotated the class with @Module, Koin identifies said class and generates an extension property.

Let’s see what the generated code looks like.

public val com_santimattius_basic_di_di_DataModule : Module = module {
val moduleInstance = com.santimattius.basic.di.di.DataModule()
single() { moduleInstance.provideMovieRemoteDataSource(retrofitServiceCreator=get()) } bind(com.santimattius.basic.di.data.datasources.MovieRemoteDataSource::class)
single() { moduleInstance.provideMovieLocalDataSource(appDataBase=get()) } bind(com.santimattius.basic.di.data.datasources.MovieLocalDataSource::class)
}
public val com.santimattius.basic.di.di.DataModule.module : org.koin.core.module.Module get() = com_santimattius_basic_di_di_DataModule

The generated code is basically a Koin Module from the DSL with the definitions we have in our class annotated as @Module.

However, what happens if we use an annotation directly, as we saw with the repository? This is where the defaultModule that we also see in the initialization comes in.

If we use the annotations directly in the class, Koin does not generate a defaultModule with those definitions, as we can see below.

public fun KoinApplication.defaultModule(): KoinApplication = modules(defaultModule)

public val defaultModule : Module = module {
single() { com.santimattius.basic.di.data.repositories.MovieRepository(remoteDataSource=get(),localDataSource=get()) }
viewModel() { com.santimattius.basic.di.MainViewModel(repository=get()) }
}

We see that the definition of our ViewModel also appears.

Once the initialization is set up, we can use the dependencies.

Disabling the Default Module (from 1.3.0)

By default, the Koin compiler detects any definition not linked to a module and places it in a “default module”, a Koin module generated at the root of your project. You can disable the use and generation of default module with the following option:

// in build.gradle or build.gradle.kts
ksp {
arg("KOIN_DEFAULT_MODULE","false")
}

Step 4: Inject dependencies into the Activities

Replace the Hilt injection in Activities with the Koin injection:

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BasicSkeletonContainer {
MainRoute()
}
}
}
}

@Composable
fun MainRoute(
viewModel: MainViewModel = koinViewModel(),
) {
val state by viewModel.state.collectAsStateWithLifecycle()
MainScreen(
state = state,
onRefresh = viewModel::refresh,
)
}

As we can see in our activity, we use koinViewModel in the same way as hiltViewModel.

Conclusion

In conclusion, both Hilt and Koin are effective and useful Dependency Injection frameworks for Android application development. While Hilt, powered by Google, offers solid integration with Dagger and AndroidX, Koin stands out for its simplicity and lightness. Although Hilt’s setup may be more complex, the recent introduction of annotations in Koin makes migration between these two frameworks easier. This document provides a detailed example of the migration process from Hilt to Koin using annotations, demonstrating that with the correct setup and annotations, the switch can be quite straightforward and direct.

Referencias

--

--