1. Overview

Koin is a popular dependency injection framework for Kotlin, known for its simplicity and powerful features. We’ve already covered the basics of Koin and its configuration using its DSL in a previous article. If you aren’t familiar with Koin concepts, we’d suggest having a look at it.

Koin Annotations were introduced to make dependency management even easier by allowing us to set up their dependencies using annotations. This approach simplifies the configuration and results in cleaner, more concise code.

In this tutorial, we’ll cover the basics of Koin Annotations, see how to set them up, and demonstrate how to use them to manage dependencies in our projects.

2. Getting Started with Koin Annotations

To start using Koin Annotations, we’ll create a basic Gradle project. We need to add the koin-core dependency itself, as well as the koin-annotations dependency to our build.gradle.kts:

dependencies {
    implementation("io.insert-koin:koin-core:3.5.6")
    implementation("io.insert-koin:koin-annotations:1.3.1")
}

The library also relies on the ksp compiler plugin, which the koin-ksp-compiler uses to generate static Koin configuration from our annotated components:

plugins {
    kotlin("jvm") version "1.9.24"
    id("com.google.devtools.ksp") version "1.9.24-1.0.20"
}
dependencies {
    // koin, other dependencies
    ksp("io.insert-koin:koin-ksp-compiler:1.3.1")
}

We can find the latest versions of the above dependencies on their official page. Note that Google’s ksp plugin version should match your Kotlin version, in our example, we use Kotlin 1.9.24.

With these dependencies added, we’re ready to use Koin Annotations in our project.

3. Koin Definitions

Koin Annotations declare the same kind of definitions as the regular Koin DSL. We’ll use the three main Koin definitions, declared using annotations:

  • @Single – declares a singleton instance and is compiled to the single() method DSL.
  • @Factory – declares a factory instance and is equivalent to the factory() method DSL.
  • @Scope – allows us to define and control the lifespan of objects beyond the default scopes mentioned above. It is compiled into scope() and scoped() method DSLs.

4. Defining Modules

In Koin, modules group related definitions together. You register modules by loading them with the static configuration DSL method modules(). We’ll see how we can declare, organize, and link our modules using annotations.

4.1. Default Module

The ksp compiler generates a default module with definitions that do not belong to any declared module. We’ll declare a singleton and load it from the default module:

@Single
class NumberService {
    fun generateRandomNumber(): Int = Random.nextInt()
}
startKoin {
    modules(defaultModule)
}

Make sure to import the default module from the generated ksp sources, in our example, we import it from org.koin.ksp.generated.defaultModule.

4.2. Module Definition

With Koin Annotations, you can define a module using the @Module annotation:

In this example, we define a DaoModule class as a module that provides us with a Database and a Repository. The @Single annotation specifies that the Database instance should be a singleton, while the @Factory annotation indicates that a new Repository instance should be created each time it is needed.

The ksp compiler will generate the static definition of such module and its extension. We load such a module by creating an instance of it and accessing its extension:

startKoin {
    modules(DaoModule().module)
}

4.3. Module Composition

We can compose modules by including them in a separate module using annotation’s include property:

@Module
class ModuleA {
    @Single
    fun provideModuleAComponent(): ModuleAComponent {
        return ModuleAComponent()
    }
}

@Module(includes = [ModuleA::class])
class ModuleB

By loading ModuleB, we’ll also load ModuleA and will be able to use its components. We may also include multiple modules in ModuleB.

4.4. Automatic Component Scanning

We can enable a @Module to automatically scan for our components and register them using @ComponentScan:

@Module
@ComponentScan("com.baeldung.kotlin.koin.annotations.domain")
class DomainModule

In our example, we define a DomainModule and configure it to scan for component definitions in the provided com.baeldung.kotlin.koin.annotations.domain package. By default, if the package is not provided, Koin will scan for components in the current package and its subpackages.

5. Scoped Definitions

Scopes in Koin manage the lifecycle of dependencies. A scope in Koin is essentially a bounded container that holds specific instances for a particular period of time or context. This is useful when we need to manage objects with a specific lifecycle tied to a particular component or task, such as a feature module, a screen in an Android app, or a user session.

5.1. Creating Scoped Components

Koin Annotations provides the @Scope annotation to define scoped components. First, we define a class for scope definition:

We then use this class definition to register a scoped component:

@Module
class ShoppingModule {
    @Scope(ShoppingSessionScope::class)
    fun provideShoppingCart() = ShoppingCart()
}

This allows us to statically create scoped ShoppingCart components using Koin’s DSL. We must first create an instance of this scope:

val shoppingScope = getKoin().createScope<ShoppingSessionScope>()

Each scope must have a globally unique scope ID. By default, Koin generates one for us, though we may pass a scopeId parameter to the createScope() method to be able to retrieve it later from the global registry using the same id.

By using the above scope, we can create, or get existing scoped components:

val cart = shoppingScope.get<ShoppingCart>()

This will create a singleton component only accessible from the ShoppingSessionScope instance.** We may also annotate the provideShoppingCart() method with a @Factory annotation if we want a new instance of a component each time.

5.2. Scope Lifecycle

Scopes are intended to be used for more fine-grained dependency management to conserve resources or improve performance. Using scopes, we may create components on demand, but we must also not forget to get rid of them when we are done using them.

When we finish working with a certain scope, it is our responsibility to evict it and its components from the global Koin registry. It is done using the scope instance itself:

shoppingScope.close()

After doing this, we’ll no longer be able to access the scope’s components. Having this in mind, we should design our scopes as small as possible, though this comes at a cost as managing them may become burdensome and complicated.

6. Conclusion

Koin Annotations offer a powerful and concise way to define and manage dependencies in Kotlin projects. Using annotations simplifies the setup of dependency injection, resulting in cleaner and more maintainable code.

By leveraging Koin Annotations, we can focus more on building the core functionality of our applications and less on the boilerplate code required for dependency injection.

For more details and advanced usage, refer to the official Koin Annotations documentation.


原始标题:Introduction to Koin Annotations