1. Introduction
Dependency injection is here to stay. Without it, it’s hard to imagine the coveted separation of concerns or proper testability.
At the same time, while Spring Framework is a prevalent choice, it’s not for everybody. Some would prefer more lightweight frameworks with better support for asynchronous IO. Some would appreciate static dependency resolution for better startup performance.
There’s always Guice, but if we want something with a more Kotlin look-and-feel, we should look at Koin. This lightweight framework provides its dependency injection capabilities through a DSL, which is hard to achieve in Java-dedicated Guice.
Apart from being an expressive way to declare dependencies between entities in our code, Koin natively supports popular Kotlin applications, such as Ktor and the Android platform. As another trade-off, Koin is “magicless” – it doesn’t generate any proxies, uses no reflection, and attempts no heuristics to find a proper implementation to satisfy our dependency. On the other hand, it will only do what it was explicitly told to do, and there will be no Spring-like “auto wiring”.
In this tutorial, we’ll study the basics of Koin and pave the way for more advanced usage of the framework.
2. How to Start With Koin
As with all libraries, we have to add some dependencies:
<dependency>
<groupId>io.insert-koin</groupId>
<artifactId>koin-core</artifactId>
<version>${3.2.0-beta-1}</version>
</dependency>
<dependency>
<groupId>io.insert-koin</groupId>
<artifactId>koin-test</artifactId>
<version>${3.2.0-beta-1}</version>
</dependency>
If we plan to use JUnit 5 in our project, we also need its dependency:
<dependency>
<groupId>io.insert-koin</groupId>
<artifactId>koin-test-junit5</artifactId>
<version>${3.2.0-beta-1}</version>
</dependency>
Similarly, for the Ktor version, there is a special dependency (it replaces the core dependency in Ktor applications):
<dependency>
<groupId>io.insert-koin</groupId>
<artifactId>koin-ktor</artifactId>
<version>${3.2.0-beta-1}</version>
</dependency>
That’s all we need to start using the library. We’ll be using the latest beta version so that the guide will stay relevant for longer.
3. Modules and Definitions
Let’s start our journey by creating a registry for our DI to use when injecting dependencies.
3.1. Modules
The modules contain declarations of dependencies between services, resources, and repositories. There can be multiple modules, a module for each semantic field. In creating the Koin context, all modules go into the modules() function, discussed later.
The modules can depend on definitions from other modules. Koin evaluates the definitions lazily. That means that the definitions can even form dependency circles. However, it would still make sense to avoid creating semantic circles since they might be hard to support in the future.
To create a module, we have to use function module {}:
class HelloSayer() {
fun sayHello() = "Hello!"
}
val koinModule = module {
single { HelloSayer() }
}
Modules can be included in one another:
val koinModule = module {
// Some configuration
}
val anotherKoinModule = module {
// More configuration
}
val compositeModule = module {
includes(koinModule, anotherKoinModule)
}
Moreover, they can form a complex tree without significant penalty at runtime. The includes() function will flatten all the definitions.
3.2. Singleton and Factory Definitions
To create a definition, most often, we will have to use a single
single<RumourTeller> { RumourMonger(get()) }
The single {} will create a definition for a singleton object and will return this same instance each time get() is called.
Another way to create a singleton is the new 3.2-version feature singleOf(). It’s based on two observations.
First, most Kotlin classes have only one constructor. Thanks to the default values, they don’t need multiple constructors to support various use cases like in Java.
Second, most of the definitions have no alternatives. In the old Koin versions, this led to definitions like:
single<SomeType> { get(), get(), get(), get(), get(), get() }
So instead, we can mention the constructor we want to invoke:
class BackLoop(val dependency: Dependency)
val someModule = module {
singleOf(::BackLoop)
}
Another verb is factory {}, which will create an instance every time the registered definition is requested:
factory { RumourSource() }
Unless we declare single {} dependency with createdAtStart = true parameter, the creator lambda will run only when a KoinComponent requests the dependency explicitly.
3.3. Definition Options
The thing that we need to understand is that every definition is a lambda. That means, that while it usually is a simple constructor invocation, it doesn’t have to be:
fun helloSayer() = HelloSayer()
val factoryFunctionModule = module {
single { helloSayer() }
}
Moreover, the definition can have a parameter:
module {
factory { (rumour: String) -> RumourSource(rumour) }
}
In the case of a singleton, the first invocation will create an instance, and all other attempts at passing the parameter will be ignored:
val singleWithParamModule = module {
single { (rumour: String) -> RumourSource(rumour) }
}
startKoin {
modules(singleWithParamModule)
}
val component = object : KoinComponent {
val instance1 = get<RumourSource> { parametersOf("I've seen nothing") }
val instance2 = get<RumourSource> { parametersOf("Jane is seeing Gill") }
}
assertEquals("I've heard that I've seen nothing", component.instance1.tellRumour())
assertEquals("I've heard that I've seen nothing", component.instance2.tellRumour())
In the case of a factory, each injection will be instantiated with its parameter, as expected:
val factoryScopeModule = module {
factory { (rumour: String) -> RumourSource(rumour) }
}
startKoin {
modules(factoryScopeModule)
}
// Same component instantiation
assertEquals("I've heard that I've seen nothing", component.instance1.tellRumour())
assertEquals("I've heard that Jane is seeing Gill", component.instance2.tellRumour())
Another trick is how to define several objects of the same type. There’s nothing more simple:
val namedSources = module {
single(named("Silent Bob")) { RumourSource("I've seen nothing") }
single(named("Jay")) { RumourSource("Jack is kissing Alex") }
}
Now we can distinguish one from another when we inject them.
4. Koin Components
*Definitions from modules are used in KoinComponents*. A class implementing KoinComponent is somewhat similar to a Spring @Component. It has a link to the global Koin instance and serves as an entry point to the object tree encoded in the modules:
class SimpleKoinApplication : KoinComponent {
private val service: HelloSayer by inject()
}
By default, the link to the Koin instance is implicit and uses GlobalContext, but this mechanism can be overridden in some cases.
We should instantiate Koin components normally, via their constructors, and not by injecting them into a module. This is the recommendation by the library authors: Probably, including components into modules incurs performance penalties or risks too deep a recursion.
4.1. Eager Evaluation vs. Lazy Evaluation
A KoinComponent has the powers to inject() or get() a dependency:
class SimpleKoinApplication : KoinComponent {
private val service: HelloSayer by inject()
private val rumourMonger: RumourTeller = get()
}
Injecting means lazy evaluation: It requires using the by keyword and returns a delegate that evaluates on the first call. Getting returns the dependency immediately.
5. Koin Instance
To activate all our definitions, we have to create a Koin instance. It can be created and registered in the GlobalContext and will be available throughout our runtime, or else we can create a standalone Koin and mind the reference to it ourselves.
To create a standalone Koin instance, we have to use koinApplication{}:
val app = koinApplication {
modules(koinModule)
}
We’ll have to preserve the reference to the application and use it later to initialize our components. The startKoin {} function creates the global instance of Koin:
startKoin {
modules(koinModule)
}
However, Koin specifically favors some frameworks. One example is Ktor. It has its way of initializing the Koin configuration. Let’s talk about setting up Koin both in a “vanilla” Kotlin application and a Ktor web server.
5.1. Basic Vanilla Koin Application
To start with a basic Koin configuration, we have to create a Koin instance:
startKoin {
logger(PrintLogger(Level.INFO))
modules(koinModule, factoryScopeModule)
fileProperties()
properties(mapOf("a" to "b", "c" to "d"))
environmentProperties()
createEagerInstances()
}
This function can be called only once per the JVM lifecycle. The most important part of it is the modules() call, where all definitions are loaded. However, we can load extra modules or unload some modules later with loadKoinModules() and unloadKoinModules().
Let’s look at other calls within the startKoin {} lambda. There is logger(), various *properties(), and a createEagerInstances() call. The first two merit their chapters, while the third creates those singletons that have the createdAtStart = true argument.
5.2. Basic Ktor Server With Koin
For a Ktor server, Koin is just another feature that we install. It plays the part of the startKoin {} call:
fun Application.module(testing: Boolean = false) {
koin {
modules(koinModule)
}
}
After that, the Application class has the powers of KoinComponent and can inject() dependencies:
routing {
val helloSayer: HelloSayer by inject()
get("/") {
call.respondText("${helloSayer.sayHello()}, world!")
}
}
5.3. Standalone Koin Instance
It might be a good idea not to use the global Koin instance for SDKs and libraries. To achieve that, we have to use the koinApplication {} starter and keep the returned reference to a Koin instance:
val app = koinApplication {
modules(koinModule)
}
Then we need to override part of the default KoinComponent functionality:
class StandaloneKoinApplication(private val koinInstance: Koin) : KoinComponent {
override fun getKoin(): Koin = koinInstance
// other component configuration
}
After that, we’ll be able to instantiate components at runtime with one additional argument – the Koin instance:
StandaloneKoinApplication(app.koin).invoke()
6. Logging and Properties
Let’s now talk about those logger() and properties() functions in our startKoin {} configuration.
6.1. Koin Logger
To facilitate the search for problems in configuration, we can enable the Koin logger. In fact, it’s always enabled, but by default, it uses its EmptyLogger implementation. We can change that to PrintLogger to see Koin logs in the standard output:
startKoin {
logger(PrintLogger(Level.INFO))
}
Alternatively, we can implement our Logger or, if we use Ktor, Spark, or Android flavors of Koin, we can use their loggers: SLF4JLogger or AndroidLogger.
6.2. Properties
Koin also can consume properties from a file (default location is classpath:koin.properties, therefore in our project, this file should be src/main/resources/koin.properties), from the system environment, and directly from a passed map:
startKoin {
modules(initByProperty)
fileProperties()
properties(mapOf("rumour" to "Max is kissing Alex"))
environmentProperties()
}
Then in a module, we can get access to these properties by getProperty() methods:
val initByProperty = module {
single { RumourSource(getProperty("rumour", "Some default rumour")) }
}
7. Testing Koin Applications
Koin also provides quite an extensive testing infrastructure. By implementing the KoinTest interface, we give our test KoinComponent powers and more:
class KoinSpecificTest : KoinTest {
@Test
fun `when test implements KoinTest then KoinComponent powers are available`() {
startKoin {
modules(koinModule)
}
val helloSayer: HelloSayer = get()
assertEquals("Hello!", helloSayer.sayHello())
}
}
7.1. Mocking Koin Definitions
One simple way to mock or otherwise replace entities declared in modules is to create an ad-hoc module when we start Koin in the test:
startKoin {
modules(
koinModule,
module {
single<RumourTeller> { RumourSource("I know everything about everyone!") }
}
)
}
Another way is to use JUnit 5 extensions for startKoin {} and mocking:
@JvmField
@RegisterExtension
val koinTestExtension = KoinTestExtension.create {
modules(
module {
single<RumourTeller> { RumourSource("I know everything about everyone!") }
}
)
}
@JvmField
@RegisterExtension
val mockProvider = MockProviderExtension.create { clazz ->
mockkClass(clazz)
}
After registering these extensions, mocking doesn’t have to be in a special module or one place. Any call to declareMock
@Test
fun when_extensions_are_used_then_mocking_is_easier() {
declareMock<RumourTeller> {
every { tellRumour() } returns "I don't even know."
}
val mockedTeller: RumourTeller by inject()
assertEquals("I don't even know.", mockedTeller.tellRumour())
}
We can use whatever mocking library or mocking approach we like, as Koin does not specify the mocking framework.
7.2. Checking Koin Modules
Koin also provides tools to check our module configuration and discover all possible problems with injection we might face at runtime. It’s easy to do: We need to either call checkModules() within the KoinApplication configuration or else check a list of modules with checkKoinModules():
koinApplication {
modules(koinModule, staticRumourModule)
checkModules()
}
Module checking has its DSL. This language allows mocking some of the values within a module or supplying a substitute value. It also allows passing parameters that the module might need to be instantiated:
koinApplication {
modules(koinModule, module { single { RumourSource() } })
checkModules {
withInstance<RumourSource>()
withParameter<RumourTeller> { "Some param" }
}
}
8. Conclusion
In this guide, we looked very closely at the Koin library and how to use it.
Koin creates a root context object that holds the configuration for instantiating dependencies and specific parameters of these dependencies. In the case of singleton objects, it also holds references to instances of these dependencies.
The dependencies can be described in modules. The modules are fed into the creator function of the Koin object. They describe how to create dependencies with producer functions that, quite often, are simply calls to constructors. The modules can depend on each other, and the individual definitions can have various scopes, such as singleton and factory. The definitions also might have parameters that allow us to inject the environment-specific data into the object tree.
To access the dependencies, we need to mark one or several objects as KoinComponent. Then we can declare fields of such objects as inject() delegates or else eagerly instantiate them with get().
During Koin creation, we can inject parameters from a file, the environment, and a programmatically created Map. We can use all three of these ways or any subset of them. In the Koin object, these properties are placed in a single registry, so the last property definition to be loaded wins.
This is also true for the modules: Overriding definitions is allowed, and the latest definition wins.
Koin provides features for testing both the functionality and the Koin configuration itself.
As usual, all our code is over on GitHub.