In previous topics, we have seen the importance of applying dependency injections to build clean code that is adaptable to changes and manage our dependencies and code coupling.
Koin is a cross-platform framework that simplifies dependency management and offers various mechanisms to provide dependencies to software components that require them.
In this topic, we will focus on another facet of Koin – defining our dependencies based on annotations to achieve the same functionality that we explained in the previous topic.
Starting with annotations
The objective of using annotations in Koin is to find an alternative mechanism to declare your modules and required dependencies quickly and intuitively. With annotations, the Koin processor generates the necessary DSL transparently for the developer. The first step is to install all the necessary elements for it.
In our gradle file (i.e., build.gradle.kts), we need to add the KSP plugin for our Kotlin version. KSP (Kotlin Symbol Processing) is a plugin for the Kotlin compiler that allows for generating code based on annotations during compilation time. It provides an alternative to annotation processors in Java, which are executed during compilation but require a separate compilation round to generate code. With KSP, you can write a Kotlin code generator that processes annotations and generates Kotlin code that can be used by your project. KSP provides a simpler API than annotation processors and can be used to generate Kotlin code more efficiently. Please use the version matching your Kotlin version. You can select the right version in the repository. For example, for Kotlin 1.8.0, we can use KSP 1.8.0-1.0.8, the first three numbers show the Kotlin compatible version.
The second step is to set up the dependencies. We need the core dependency, like the DSL version (we use it internally), the annotations dependency, and the KSP dependency for Koin. Optionally, we can also specify the destination for the generated code.
plugins {
kotlin("jvm") version "1.8.0"
id("com.google.devtools.ksp") version "1.8.0-1.0.8"
}
// ....
dependencies {
// ...
// Koin
implementation("io.insert-koin:koin-core:$koin_version") // Koin Core
implementation("io.insert-koin:koin-annotations:$koin_ksp_version") // Annotations
ksp("io.insert-koin:koin-ksp-compiler:$koin_ksp_version") // Koin KSP
// ...
}
// Optional: KSP - To use generated sources
sourceSets.main {
java.srcDirs("build/generated/ksp/main/kotlin")
}Definitions
Koin Annotations enable us to declare definitions in the same way as with the regular Koin DSL, but using annotations instead. To do so, simply annotate your class with the appropriate annotation, and the necessary code will be generated automatically.
For instance, instead of writing a DSL declaration like single { MyClass(get()) }, you can achieve the same result by annotating the class with @Single.
@Single
class MyClass(val myDependency : MyDependency)
Koin Annotations keep the same semantic as the Koin DSL. You can declare your components with the following definitions:
@Single: a singleton instance (declared withsingle { }in DSL)@Factory: a factory instance, for instances recreated each time you need an instance (declared withfactory { }in DSL).
When defining a component, all "bindings" (related supertypes) that are detected will be automatically prepared for you. For instance, consider the following declaration:
@Single
class MyClass(val myDependency : MyDependency) : MyInterface
Koin will establish a connection between your MyClass component and MyInterface as well. The equivalent Koin DSL declaration is single { MyComponent(get()) } bind MyInterface::class. Alternatively, instead of relying on Koin's automatic detection, you can specify the type to which you want to bind using the binds annotation parameter:
@Single(binds = [MyInterface::class])
class MyClass(val myDependency : MyDependency) : MyInterface
If your component requires a nullable dependency, there's no need to worry because Koin will handle it automatically. Simply use your definition annotation as usual, and Koin will determine what to do. The following example is equivalent to the DSL declaration single { MyComponent(getOrNull()) }:
@Single
class MyClass(val myDependency : MyDependency?)
The @Named annotation allows you to assign a "name", or a qualifier, to a definition, enabling you to differentiate between multiple definitions for the same type.
@Single
@Named("MemoryStorage")
class MemoryStorage : Storage
@Single
@Named("DatabaseStorage")
class Databasestorage(private val logDao: LogDao) : Storage
To resolve a dependency, you can use the named function along with the qualifier:
val storage: Storage by inject(named("MemoryStorage"))Definitions: injected parameters
You can designate a constructor parameter as an injected parameter, which indicates that the dependency will be passed to the graph when resolving it. Here's an example:
@Single
class MyClass(@InjectedParam val myDependency : MyDependency)
Then, you can call MyClass and pass an instance of MyDependency:
val m = MyDependency
// Resolve MyComponent while passing MyDependency
koin.get<MyClass> { parametersOf(m) }
The equivalent DSL declaration would be: single { params -> MyClass(params.get()) }.
Koin provides support for injecting lazy dependencies using the Lazy<T> type in Kotlin. In the following example, we want to resolve the Storage definition lazily. To do that, we can simply declare the constructor parameter with the Lazy<T> type:
@Single
class MemoryStorage : Storage
@Single
class Repository(val storage: Lazy<Storage>)
The generated DSL code will use the inject() function instead of get(): single { Repository(inject()) }.
Koin can automatically detect and resolve all the dependencies in a list. For example, let's say we want to resolve all definitions of the Storage type. To achieve this, you just need to use the Kotlin List type as follows:
@Single
@Named("MemoryStorage")
class MemoryStorage : Storage
@Single
@Named("Databasestorage")
class Databasestorage(private val logDao: LogDao) : Storage
@Single
class Repository(val datasource : List<Storage>)
Behind, it will generate the DSL like with the getAll() function: single { Repository(getAll()) }.
Definitions: properties and scopes
To resolve a Koin property in your definition, you can tag a constructor member with @Property. This will allow Koin to resolve the property using the value passed to the annotation:
@Single
class MyClass(@Property("my_key") val myProperty : String)
The generated DSL equivalent will be: single { MyClass(getProperty("my_key")) }.
You can load a properties file when Koin is initialized:
startKoin {
// Load properties from the default location
// (i.e. `/src/main/resources/koin.properties`)
fileProperties()
// Load properties from the application.properties file
// (i.e. `/src/main/resources/application.properties`)
// fleProperties(./application.properties)
}
To declare a definition inside a scope, use the @Scope annotation, which allows you to specify the target scope as a class or a name:
// scope by type
@Scope(MyScope::class)
class MyComponent
// scope by name
@Scope(name = "MyScopeName")
class MyComponent
The generated DSL equivalent will be:
scope<MyScope> {
scoped { MyComponent() }
}
// or
scope(named("MyScopeName")) {
scoped { MyComponent() }
}Modules
When working with definitions in Koin, you have the option to organize them into modules. You can even skip creating a module and use the default one that's generated for you.
If you don't want to create a module, Koin provides a default one that can be used to host all your definitions. You can use the defaultModule directly in your code (don't forget to import org.koin.ksp.generated.*):
// Use Koin Generation
import org.koin.ksp.generated.*
fun main() {
startKoin {
defaultModule()
}
}
// or
fun main() {
startKoin {
modules(
defaultModule
)
}
}
To create a module, you just need to annotate a class with the @Module annotation:
@Module
class MyModule
To load your module in Koin, you can use the .module extension function, which is generated for any class annotated with @Module. Simply create a new instance of your module and call .module (don't forget to import org.koin.ksp.generated.*):
// Use Koin Generation
import org.koin.ksp.generated.*
fun main() {
startKoin {
modules(
MyModule().module
)
}
}
If you have annotated components that you want to gather into a module, you can use the @ComponentScan annotation on the module:
@Module
@ComponentScan
class MyModule
This will scan the current package and its sub-packages for the annotated components. You can specify a different package to scan by using @ComponentScan("com.my.package"). When using @ComponentScan, KSP will only scan the current Gradle source, not across multiple modules.
To make a definition directly in your module class, you can annotate a function with the appropriate definition annotation:
// given
// class MyComponent(val myDependency : MyDependency)
@Module
class MyModule {
@Single
fun myComponent(myDependency : MyDependency) = MyComponent(myDependency)
}
You can also use the @InjectedParam and @Property annotations on function members.
To include other class modules in your module, you can use the includes attribute of the @Module annotation:
@Module
class ModuleA
@Module(includes = [ModuleA::class])
class ModuleB
This way, you can load your root module:
// Use Koin Generation
import org.koin.ksp.generated.*
fun main() {
startKoin {
modules(
// will load ModuleB & ModuleA
ModuleB().module
)
}
}Conclusion
In this topic, we have learned how to use Koin Annotations in our code to help us organize and work with dependencies to generate a low-coupled architecture.
Now is the time to do some tasks to check what you have learned. Are you ready?