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.
About the author
Matthew Casperson
Product Manager at Octopus Deploy