mscharhag, Programming and Stuff;

A blog about programming and software development topics, mostly focused on Java technologies including Java EE, Spring and Grails.

Monday, 8 March, 2021

Kotlin dependency injection with Koin

Dependency injection is a common technique in today's software design. With dependency injection we pass dependencies to a component instead of creating it inside the component. This way we can separate construction and use of dependencies.

In this post we will look at Koin, a lightweight Kotlin dependency injection library. Koin describes itself as a DSL, a light container and a pragmatic API.

Getting started with Koin

We start with adding the Koin dependency to our project:

<dependency>
    <groupId>org.koin</groupId>
    <artifactId>koin-core</artifactId>
    <version>2.2.2</version>
</dependency>

Koin artifacts are available on jcenter.bintray.com. If not already available you can add this repository with:

<repositories>
    <repository>
        <id>central</id>
        <name>bintray</name>
        <url>https://jcenter.bintray.com</url>
    </repository>
</repositories>

Or if you are using Gradle:

repositories {
    jcenter()    
}

dependencies {
    compile "org.koin:koin-core:2.2.2"
}

Now let's create a simple UserService class with a dependency to an AddressValidator object:

class UserService(
    private val addressValidator: AddressValidator
) {
    fun createUser(username: String, address: Address) {
        // use addressValidator to validate address before creating user
    }
}

AddressValidator simply looks like this:

class AddressValidator {
    fun validate(address: Address): Boolean {
        // validate address
    }
}

Next we will use Koin to wire both components together. We do this by creating a Koin module:

val myModule = module {
    single { AddressValidator() }
    single(createdAtStart = true) { UserService(get()) }
}

This creates a module with two singletons (defined by the single function). single accepts a lambda expression as parameter that is used to create the component. Here, we simply call the constructors of our previously defined classes.

With get() we can resolve dependencies from a Koin module. In this example we use get() to obtain the previously defined AddressValidator instance and pass it to the UserService constructor.

The createdAtStart option tells Koin to create this instance (and its dependencies) when the Koin application is started.

We start a Koin application with:

val app = startKoin {
    modules(myModule)
}

startKoin launches the Koin container which loads and initializes dependencies. One or more Koin modules can be passed to the startKoin function. A KoinApplication object is returned.

Retrieving objects from the Koin container

Sometimes it necessary to retrieve objects from the Koin dependency container. This can be done by using the KoinApplication object returned by the startKoin function:

// retrieve UserService instance from previously defined module
val userService = app.koin.get<UserService>()

Another approach is to use the KoinComponent interface. KoinComponent provides an inject method we use to retrieve objects from the Koin container. For example:

class MyApp : KoinComponent {
   
    private val userService by inject<UserService>()

    ...
}

Factories

Sometimes object creation is not as simple as just calling a constructor. In this case, a factory method can come in handy. Koin's usage of lambda expressions for object creation support us here. We can simply call factory functions from the lambda expression.

For example, assume the creation of a UserService instance is more complex. We can come up with something like this:

val myModule = module {

    fun provideUserService(addressValidator: AddressValidator): UserService {
        val userService = UserService(addressValidator)
        // more code to configure userService
        return userService
    }

    single { AddressValidator() }
    single { provideUserService(get()) }
}

As mentioned earlier, single is used to create singletons. This means Koin creates only one object instance that is then shared by other objects.

However, sometimes we need a new object instance for every dependency. In this case, the factory function helps us:

val myModule = module {
    factory { AddressValidator() }
    single { UserService(get()) }
    single { OtherService(get()) } // OtherService constructor takes an AddressValidator instance
}

With factory Koin creates a new AddressValidator objects whenever an AddressValidator is needed. Here, UserService and OtherService get two different AddressValidator instances via get().

Providing interface implementations

Let's assume AddressValidator is an interface that is implemented by AddressValidatorImpl. We can still write our Koin module like this:

val myModule = module {
    single { AddressValidatorImpl() }
    single { UserService(get()) }
}

This defines a AddressValidatorImpl instance that can be injected to other components. However, it is likely that AddressValidatorImpl should only expose the AddressValidator interface. This way we can enforce that other components only depend on AddressValidator and not on a specific interface implementation. We can accomplish this by adding a generic type to the single function:

val myModule = module {
    single<AddressValidator> { AddressValidatorImpl() }
    single { UserService(get()) }
}

This way we expose only the AddressValidator interface by creating a AddressValidatorImpl instance.

Properties and configuration

Obtaining properties from a configuration file is a common task. Koin supports loading property files and giving us the option to inject properties.

First we need to tell Koin to load properties which is done by using the fileProperties function. fileProperties has an optional fileName argument we can use to specify a path to a property file. If no argument is given Koin tries to load koin.properties from the classpath.

For example:

val app = startKoin {
   
    // loads properties from koin.properties
    fileProperties()
    
    // loads properties from custom property file
    fileProperties("/other.properties")
    
    modules(myModule)
}

Assume we have a component that requires some configuration property:

class ConfigurableComponent(val someProperty: String)

.. and a koin.properties file with a single entry:

foo.bar=baz

We can now retrieve this property and inject it to ConfigurableComponent by using the getProperty function:

val myModule = module {
    single { ConfigurableComponent(getProperty("foo.bar")) }
}

Summary

Koin is an easy to use dependency injection container for Kotlin. Koin provides a simple DSL to define components and injection rules. We use this DSL to create Koin modules which are then used to initialize the dependency injection container. Koin is also able to inject properties loaded from files.

For more information you should visit the Koin documentation page. You can find the sources for this post on GitHub.

Leave a reply