Koin is my favourite DI library, but out of the box it’s a bit verbose. With Koin Annotations, the overhead of manually writing module{}
declarations is removed, making it as easy to use as Android’s Hilt library. Here’s how to use Koin Annotations in a multiplatform project.
Basic usage
Koin Annotations works with a KSP processor that scans for specific annotations and then creates a module{}
declaration, as if you would normally write manually. If you’re not familiar with KSP, just know it’s a compiler plugin mechanism that allows for code generation.
I’ll assume starting with a project that’s already configured with Koin, but this article can also serve as a first start guide. Feel free to reply if there are any questions.
1: Configuring the build
First, declare the libraries:
[versions]
koin = "3.5.3"
koin-ksp = "1.3.1"
ksp = "1.9.22-1.0.17"
[libraries]
koin-annotations = { module = "io.insert-koin:koin-annotations", version.ref = "koin-ksp" }
koin-compiler = { module = "io.insert-koin:koin-ksp-compiler", version.ref = "koin-ksp" }
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" }
[plugins]
ksp = { id ="com.google.devtools.ksp", version.ref = "ksp" }
Then use these dependencies:
plugins {
alias(libs.plugins.ksp)
}
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.koin.annotations)
implementation(libs.koin.core)
}
}
}
dependenies {
add("kspCommonMainMetadata", libs.koin.compiler)
// Other compilation targets should be added here, see up in article.
}
For Android, also add implementation(libs.koin.android)
in androidMain.dependencies{}
.
In older Kotlin/KSP versions, it was necessary to manually add the KSP output as a source directory with kotlin.srcDir("build/generated/ksp/metadata/commonMain/kotlin")
. That’s no longer required as it’s automatically configured now. Same goes for the dependsOn kspCommonMainKotlinMetadata
workaround you might find online. That’s why it’s recommended to use the latest dependencies.
2: Replace/add the modules to @Modules
Koin Annotations works by adding a class annotated with @Module
in the same gradle module as where the the things to be injected also reside. Here’s how that looks.
You might currently have a manually written module:
val myModule = module {
single<MyRepository> { MyRepositoryImpl(get(), get(), get()) }
singleOf(::AnotherClass)
factoryOf(::MoreThings)
factoryOf(::OneLinePerClass)
}
This can be replaced with:
@Module
@ComponentScan
class MyModule
Now any class/function that we annotate with @Factory
(for factory instances) or @Single
(for singletons) inside this gradle module will be picked up by the KSP processor.
@Single
class MyRepositoryImpl(
private val somethingInjected: AnotherClass
) : MyRepository
Note that it’s not needed to specify that MyRepositoryImpl
should be provided when the MyRepository
interface is requested to be injected somewhere. Koin binds it automatically. So with this setup, the only thing needed to add something to DI is just one annotation in most cases.
Also, if you’re used to Hilt, you might be tempted to write @Inject constructor
. That’s not needed with Koin, the single annotation above the class is enough.
⚠️ @ComponentScan
works by scanning from the current package into deeper packages. If you place your DI stuff in a package com.example.mymodule.di
, then it won’t find code in the package com.example.mymodule
, even though it’s in the same gradle module. In that case, you can instruct the processor on where it should look by changing the annotation to @ComponentScan("com.example.mymodule")
.
3: Use the generated modules
After a build, the generated modules are available and we can use them in our Koin init. We can check out that generated code to see if everything went well so far! It’s easy to check what’s being generated by navigating to /build/generated/ksp/[target]/kotlin
. Here’s how it looks in one of my projects:
Now let’s reference the generated module. Locate your startKoin
function call and update it from this:
import org.koin.core.context.startKoin
startKoin { modules(myModule) }
to this:
import org.koin.core.context.startKoin
import org.koin.ksp.generated.module // Import for the generated modules
startKoin { modules(MyModule().module) }
Note that the module is referenced as a class instantiation MyModule()
and we then use .module
on it to get the generated module. This is because the code Koin generates is placed in the org.koin.ksp.generated.module
package.
Now run the app and see the result. That’s all there is to it!
To summarise:
- Add KSP + Koin Annotations processor
- Replace manual modules with
@Module
/@ComponentScan
+ annotate classes with@Single
/@Factory
- Reference the generated modules from your Koin start method
Advanced usage
Including Modules
You can include Modules in each other. For example, a NetworkModule
might include a separate module that provides Json (de)serialisation support. Including is done through the annotation:
@Module(includes = [JsonModule::class]
@ComponentScan
class NetworkModule
Now only including NetworkModule
is enough to also get the content of JsonModule
in the DI graph.
Extra configuration in Modules
If a class requires something that cannot be automatically provided, you can always manually write the code.
@Module
class MyModule {
@Single
fun provideSomething(dep: SomeDependency): Something {
return Something(dep, "extraConfig")
}
}
Extra configuration in methods
Alternatively, you can also just write a function marked with one of the annotations.
@Module
@ComponentScan
class MyModule
@Single
internal fun provideSomething(dep: SomeDependency): Something {
// Configuration here...
}
It needs to be in the same gradle module, but not in the same file. This will be picked up by the processor and result in generated code that looks something like the following:
public val MyModule.module: Module = module { single() { Something (dep=get()) } }
Platform-specific code
If you have platform-specific code, you need to run the KSP processor on that target (see Using KSP with KMP: Basic KSP API). The generated module will only be available inside source sets with the same target.
For example, inside [myModule/src/commonMain/kotlin]
you can have this:
@Module
@ComponentScan
class NetworkModule
And inside the platform specific module for Android 🤖:
@Single
internal fun provideAndroidHttpClient(context: Context) = HttpClient {
// Extra configuration here...
}
and for iOS 🍎:
@Single
internal fun provideAppleHttpClient() = HttpClient {
// Extra configuration here...
}
If you now compile the project on Android, NetworkModule
will be generated in build/generated/ksp/android/androidDebug/kotlin/org.koin.ksp.generated/NetworkModuleGen[package].kt
.
And if you compile the project on iOS, NetworkModule
will be generated in build/generated/ksp/IosSimulatorArm64/IosSimulatorArm64Main/kotlin/org.koin.ksp.generated/NetworkModuleGen[package].kt
.
Any annotated classes inside the [common] code will be available in both platform-specific variants of the module.
If on a higher level you have an AndroidModule
and IOSModule
, you can simply refer the common module declaration by writing includes = [NetworkModule::class]
and the right platform specific generated one will be used automatically.
For Android, if you use koin-android
and include androidContext(this@Application)
in your startKoin{}
block, then you can inject Context
anywhere you need it, as its provided by default.
ViewModel and WorkManager on Android
If you’re on Android, you might also have ViewModel
s and Worker
s you’d like to easily inject using annotations. The process is similar as above, with a difference in the annotations required. Instead of @Single
and @Factory
:
- Use
@KoinViewModel
forViewModel
s - Use
@KoinWorker
forWorker
s
These two are available by default in the Koin Annotations library, but note that they’re only available in the androidMain
source set.
Scoping modules
Like Koin’s core library, Koin Annotations also has support for scoping with a special @Scope
and @Scoped
annotation, but those are .. out of scope for this article 🥁. See Scopes in Koin Annotations.
Parameters
If you need access to dependencies only accessible on runtime, you can use Koin’s parameter mechanism. The annotation is called @InjectedParam
and its usage is explained here: Injected Parameters.
Further reading
You can reply to this article at https://medium.com/@jacobras/migrating-to-koin-annotations-in-a-multiplatform-project-1e83ba3b5988.