Koin: Lightweight Dependency Injection Framework
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.
How to Start With Koin
As with all libraries, we have to add some dependencies. Depending on the project, we will need either a vanilla Kotlin setup or a Ktor. For Gradle Kotlin DSL, there are two dependencies to make it work in the vanilla mode:
val koin_version = "3.2.0-beta-1"
implementation("io.insert-koin:koin-core:$koin_version")
testImplementation("io.insert-koin:koin-test:$koin_version")If we plan to use JUnit 5 in our project, we also need its dependency:
testImplementation("io.insert-koin:koin-test-junit5:$koin_version")Similarly, for the Ktor version, there is a special dependency (it replaces the core dependency in Ktor applications):
implementation("io.insert-koin:koin-ktor:$koin_version")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.
Koin important keywords/functions
- startKoin : Create and register KoinApplication instance
- modules: Declare used modules
- androidContext: Use the given Android context
- by inject(): allows to retrieve instances lazily
- get(): function to retrieve directly an instance (non-lazy)
- koinComponent : For using koin features, tag the class with the same to get access to koin functions
Koin Scopes
- single: creates an object that persistent with the entire container lifetime
- factory: creates a new object each time. No persistence in the container
- scoped: creates an object that persists to associated scope lifetime
Modules and Definitions
Let’s start our journey by creating a registry for our DI to use when injecting dependencies.
Module
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.
Singleton and Factory Definitions
To create a definition, most often, we will have to use a single<T>{} function, where T is a type that should match a requested type in later get<T>() calls:
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.
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") }
factory(named("ApiUrl")){Url("https://api.github.com")}
}Now we can distinguish one from another when we inject them.
Koin Components
Definitions from modules are used in KoinComponents. Koin is a DSL to help describe your modules & definitions, a container to make definition resolution. What we need now is an API to retrieve our instances outside of the container. That’s the goal of Koin components.
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:
To give a class the capacity to use Koin features, we need to tag it with KoinComponent interface. Let's take an example.
A module to define MyService instance
class MyService
val myModule = module {
// Define a singleton for MyService
single { MyService() }
}we start Koin before using definition.
Start Koin with myModule
fun main(vararg args : String){
// Start Koin
startKoin {
modules(myModule)
}
// Create MyComponent instance and inject from Koin container
MyComponent()
}Here is how we can write our MyComponent to retrieve instances from Koin container.
Use get() & by inject() to inject MyService instance
class MyComponent : KoinComponent {
// lazy inject Koin instance
val myService : MyService by inject()
// or
// eager inject Koin instance
val myService : MyService = get()
}Once you have tagged your class as KoinComponent, you gain access to:
by inject()- lazy evaluated instance from Koin containerget()- eager fetch instance from Koin containergetProperty()/setProperty()- get/set property
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.
If any dependencies needed to be used in non UI related classes, we can use KoinComponent. As per Koin docs, you can find the below.
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.
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)
}"); background-repeat: no-repeat; background-position: center center; background-color: rgb(209, 169, 74);">CopyWe’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)
}"); background-repeat: no-repeat; background-position: center center; background-color: rgb(209, 169, 74);">CopyHowever, Koin specifically favors some frameworks. One example is Ktor. It has its way of initializing the Koin configuration.
