close icon
Kotlin

Dependency Injection with Kotlin and Koin

Learn how to implement common dependency injection scenarios with Koin and Kotlin

September 21, 2021

Inversion of Control (IoC) is a broad term to describe how responsibility for some aspect of a system is lifted out of the custom code written by the end developer and into a framework. Martin Fowler describes a framework in terms of IoC:

Inversion of Control is a key part of what makes a framework different from a library. A library is essentially a set of functions that you can call, these days usually organized into classes. Each call does some work and returns control to the client.

A framework embodies some abstract design, with more behavior built-in. In order to use it, you need to insert your behavior into various places in the framework, either by subclassing or by plugging in your own classes. The framework's code then calls your code at these points.

Dependency injection (DI) is one specific example of IoC where classes no longer directly instantiate member properties by creating new objects but instead declare their dependencies and allow an external system, in this case, a dependency injection framework to satisfy those dependencies.

Koin is a dependency injection framework for Kotlin. It is lightweight, can be used in Android applications, is implemented via a concise DSL, and takes advantage of Kotlin features like delegate properties rather than relying on annotations.

In this post, we'll look at a simple application taking advantage of Koin to inject dependencies into our custom classes.

Prerequisites

To build the sample application, you'll need to have JDK 11 or above, which is available from many sources, including OpenJDK, AdoptOpenJDK, Azul, or Oracle.

The code for the sample Koin application can be found here.

The Gradle Project Definition

We start with the Gradle build file, which includes dependencies for Kotlin and Koin, and makes use of the shadow plugin to create self-contained uberjars:

buildscript {
    repositories {
        maven {
            url "https://plugins.gradle.org/m2/"
        }
    }
    dependencies {
        classpath 'com.github.jengelman.gradle.plugins:shadow:6.1.0'
    }
}

plugins {
    id "org.jetbrains.kotlin.jvm" version "1.5.21"
}

apply plugin: 'kotlin'
apply plugin: 'com.github.johnrengelman.shadow'

repositories {
    mavenLocal()
    mavenCentral()
}

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib:1.5.21"
    implementation "io.insert-koin:koin-core:3.1.2"
}

To build the uberjar, run this command from Bash or PowerShell:

./gradlew shadowJar

Registering Singletons

The first demonstration of Koin will register a class as a singleton, ensuring each time we request a new instance of the class, we are returned a single, shared object. Here is the code from the file single.kt:

// src/main/kotlin/com/matthewcasperson/single.kt

package com.matthewcasperson

import org.koin.core.context.GlobalContext.startKoin
import org.koin.dsl.module

class SingleInstance {
    companion object {
        var count: Int = 0
    }

    init {
        ++count
    }

    fun hello() = "I am instance number $count"
}

fun main() {
    val singleModule = module {
        single { SingleInstance() }
    }

    var app = startKoin {
        modules(singleModule)
    }

    println(app.koin.get<SingleInstance>().hello())
    println(app.koin.get<SingleInstance>().hello())
    println(app.koin.get<SingleInstance>().hello())
}

This class is run with the command:

java -cp ./build/libs/KotlinKoinExample-all.jar com.matthewcasperson.SingleKt

We start by defining a typical class, but with a companion object containing a variable called count. The count variable is incremented by 1 each time we create a new SingleInstance object, which we will use to track how many new SingleInstance objects have been created:

class SingleInstance {
    companion object {
        var count: Int = 0
    }

    init {
        ++count
    }

    fun hello() = "I am instance number $count"
}

Inside the main function we create a Koin module. Modules are used to group related Koin definitions, and here we use the single definition to instruct Koin to create a single instance of the supplied object:

fun main() {
    val singleModule = module {
        single { SingleInstance() }
    }

Next we call the startKoin function, which is part of the GlobalContext object. GlobalContext is a singleton (defined as an object declaration), and is typically used as the default context for applications. Here we register our module into the global context:

    var app = startKoin {
        modules(singleModule)
    }

We're now able to request instances of any of our registered objects with app.koin.get. To demonstrate that our single definitions are working as expected, we get an instance of the SingleInstance class three times and print the message containing the instance count to the console:

    println(app.koin.get<SingleInstance>().hello())
    println(app.koin.get<SingleInstance>().hello())
    println(app.koin.get<SingleInstance>().hello())
}

The output shows we have been given the same SingleInstance object each time:

I am instance number 1
I am instance number 1
I am instance number 1

Registering a Factory

There are times when you want a new instance each time you request a dependency from Koin. To support this, Koin has a factory definition. This is demonstrated in the file factory.kt:

// src/main/kotlin/com/matthewcasperson/factory.kt

package com.matthewcasperson

import org.koin.core.context.GlobalContext.startKoin
import org.koin.dsl.module

class FactoryInstance {
    companion object {
        var count: Int = 0
    }

    init {
        ++count
    }

    fun hello() = "I am instance number $count"
}

fun main() {
    val factoryModule = module {
        factory { FactoryInstance() }
    }

    var app = startKoin {
        modules(factoryModule)
    }

    println(app.koin.get<FactoryInstance>().hello())
    println(app.koin.get<FactoryInstance>().hello())
    println(app.koin.get<FactoryInstance>().hello())
}

This class is run with the command:

java -cp ./build/libs/KotlinKoinExample-all.jar com.matthewcasperson.FactoryKt

This code is almost a line for line copy of the previous example, with some different class and variable names. The most significant difference is how the module is built, where we use the factory definition:

    val factoryModule = module {
        factory { FactoryInstance() }
    }

Whereas the single definition registered a singleton dependency, the factory definition calls the supplied lambda every time a dependency is requested.

This is reflected in the console output, which shows that we have indeed constructed three instances, one for each call to app.koin.get:

I am instance number 1
I am instance number 2
I am instance number 3

Registering Interfaces

The previous two examples registered concrete classes with Koin, but good object-oriented practice is to work with interfaces rather than classes. The example below from the file interfaces.kt shows how to register a class via its base interface with Koin:

// src/main/kotlin/com/matthewcasperson/interfaces.kt

package com.matthewcasperson

import org.koin.core.context.GlobalContext.startKoin
import org.koin.dsl.module

interface HelloService {
    fun hello(): String
}

class HelloServiceImpl : HelloService {
    override fun hello() = "Hello!"
}

fun main() {
    val helloService = module {
        single { HelloServiceImpl() as HelloService }
    }

    var app = startKoin {
        modules(helloService)
    }

    println(app.koin.get<HelloService>().hello())
}

This class is run with the command:

java -cp ./build/libs/KotlinKoinExample-all.jar com.matthewcasperson.InterfacesKt

We start with a basic interface:

interface HelloService {
    fun hello(): String
}

We then implement the interface in a class:

class HelloServiceImpl : HelloService {
    override fun hello() = "Hello!"
}

To make the class available to Koin via its interface, we cast the new object back to the interface with the as operator while building the module:

    val helloService = module {
        single { HelloServiceImpl() as HelloService }
    }

We then retrieve a dependency from its interface:

    println(app.koin.get<HelloService>().hello())

Resolving Nested Dependencies

All the previous examples have resolved objects with no additional dependencies. A more typical scenario is where Koin is used to resolve classes that themselves have additional dependencies. This is demonstrated in the file called nested.kt:

// src/main/kotlin/com/matthewcasperson/nested.kt

package com.matthewcasperson

import org.koin.core.context.GlobalContext.startKoin
import org.koin.dsl.module

data class HelloMessageData(val message : String = "Hello from wrapped class!")

interface HelloServiceWrapper {
    fun hello(): String
}

class HelloServiceWrapperImpl(private val helloMessageData:HelloMessageData) : HelloServiceWrapper {
    override fun hello() = helloMessageData.message
}

fun main() {
    val helloService = module {
        single { HelloMessageData() }
        single { HelloServiceWrapperImpl(get()) as HelloServiceWrapper }
    }

    var app = startKoin {
        modules(helloService)
    }

    println(app.koin.get<HelloServiceWrapper>().hello())
}

This class is run with the command:

java -cp ./build/libs/KotlinKoinExample-all.jar com.matthewcasperson.NestedKt

We start with a data class defining a string property:

data class HelloMessageData(val message : String = "Hello from wrapped class!")

As with the previous example, we define an interface and then implement the interface with a class. This time, however, the class has a constructor that takes an instance of HelloMessageData:

interface HelloServiceWrapper {
    fun hello(): String
}

class HelloServiceWrapperImpl(private val helloMessageData:HelloMessageData) : HelloServiceWrapper {
    override fun hello() = helloMessageData.message
}

When defining the module, we register an instance of the HelloMessageData class, and then resolve that class in the HelloServiceWrapperImpl constructor with a call to get, which will return the appropriate dependency for us. Note the order is not important here, and HelloServiceWrapperImpl could have been defined in the module first:

    val helloService = module {
        single { HelloMessageData() }
        single { HelloServiceWrapperImpl(get()) as HelloServiceWrapper }
    }

Creating a KoinComponent

We noted earlier that Koin creates a default global context that our dependencies are registered with. Koin uses this global context, in conjunction with Kotlin delegated properties, through the KoinComponent interface to allow classes to resolve their own dependencies without an explicit reference to the KoinApplication returned by startKoin. An example of this is shown in the file koinComponent.kt:

// src/main/kotlin/com/matthewcasperson/koinComponent.kt

package com.matthewcasperson

import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.core.context.GlobalContext.startKoin
import org.koin.dsl.module

data class GoodbyeMessageData(val message : String = "Goodbye!")

interface GoodbyeService {
    fun goodbye(): String
}

class GoodbyeServiceImpl(private val goodbyeMessageData: GoodbyeMessageData) : GoodbyeService {
    override fun goodbye() = "GoodbyeServiceImpl says: ${goodbyeMessageData.message}"
}

class GoodbyeApplication : KoinComponent {
    val goodbyeService by inject<GoodbyeService>()
    fun sayGoodbye() = println(goodbyeService.goodbye())
}

fun main() {
    val goodbyeModule = module {
        single { GoodbyeMessageData() }
        single { GoodbyeServiceImpl(get()) as GoodbyeService }
    }

    startKoin {
        modules(goodbyeModule)
    }

    GoodbyeApplication().sayGoodbye()
}

This class is run with the command:

java -cp ./build/libs/KotlinKoinExample-all.jar com.matthewcasperson.KoinComponentKt

This example draws from the features demonstrated in previous sections to define a class called GoodbyeServiceImpl, with a nested dependency on the data class called GoodbyeMessageData, and which implements an interface called GoodbyeService:

data class GoodbyeMessageData(val message : String = "Goodbye!")

interface GoodbyeService {
    fun goodbye(): String
}

class GoodbyeServiceImpl(private val goodbyeMessageData: GoodbyeMessageData) : GoodbyeService {
    override fun goodbye() = "GoodbyeServiceImpl says: ${goodbyeMessageData.message}"
}

We then define a class called GoodbyeApplication implementing the KoinComponent interface. This class has a delegate property called goodbyService initialized by the inject function made available through the KoinComponent interface:

class GoodbyeApplication : KoinComponent {
    val goodbyeService by inject<GoodbyeService>()
    fun sayGoodbye() = println(goodbyeService.goodbye())
}

The module is defined in much the same way as it has been in previous examples. Note however that the GoodbyeApplication class is not defined in the module:

    val goodbyeModule = module {
        single { GoodbyeMessageData() }
        single { GoodbyeServiceImpl(get()) as GoodbyeService }
    }

In this example, we don't assign the result of the startKoin function to any variable; registering the module with the global context is enough here:

    startKoin {
        modules(goodbyeModule)
    }

We then create a new instance of the GoodbyeApplication class and call its sayGoodbye function. By implementing the KoinComponent interface, the GoodbyeApplication class can resolve its own dependencies from the global context and will resolve its GoodbyeService dependency in order to print a message to the console:

    GoodbyeApplication().sayGoodbye()

The KoinComponent interface is convenient, but be aware that it means your classes are now dependant on the Koin framework. Constructor-based injection is recommended when you wish to share code without any explicit dependency on Koin.

Conclusion

Koin is a lightweight dependency injection framework with a concise DSL taking advantage of Kotlin's modern syntax and features. In this post, we looked at how Koin creates singletons and factories, registers dependencies against their interfaces, and allows classes to resolve their own dependencies with delegated properties.

  • Twitter icon
  • LinkedIn icon
  • Faceboook icon