1. Overview
In this article, we’ll introduce Kodein — a pure Kotlin dependency injection (DI) framework — and compare it with other popular DI frameworks.
2. Dependency
First, let’s add the Kodein dependency to our pom.xml:
<dependency>
<groupId>com.github.salomonbrys.kodein</groupId>
<artifactId>kodein</artifactId>
<version>4.1.0</version>
</dependency>
Please note that the latest available version is available on Maven Central.
3. Configuration
We’ll use the model below for illustrating Kodein-based configuration:
class Controller(private val service : Service)
class Service(private val dao: Dao, private val tag: String)
interface Dao
class JdbcDao : Dao
class MongoDao : Dao
4. Binding Types
The Kodein framework offers various binding types. Let’s take a closer look at how they work and how to use them.
4.1. Singleton
With Singleton binding, a target bean is instantiated lazily on the first access and re-used on all further requests:
var created = false;
val kodein = Kodein {
bind<Dao>() with singleton {
created = true
MongoDao()
}
}
assertThat(created).isFalse()
val dao1: Dao = kodein.instance()
assertThat(created).isFalse()
val dao2: Dao = kodein.instance()
assertThat(dao1).isSameAs(dao2)
Note: we can use Kodein.instance() for retrieving target-managed beans based on a static variable type.
4.2. Eager Singleton
This is similar to the Singleton binding. The only difference is that the initialization block is called eagerly:
var created = false;
val kodein = Kodein {
bind<Dao>() with eagerSingleton {
created = true
MongoDao()
}
}
assertThat(created).isTrue()
val dao1: Dao = kodein.instance()
val dao2: Dao = kodein.instance()
assertThat(dao1).isSameAs(dao2)
4.3. Factory
With Factory binding, the initialization block receives an argument, and a new object is returned from it every time:
val kodein = Kodein {
bind<Dao>() with singleton { MongoDao() }
bind<Service>() with factory { tag: String -> Service(instance(), tag) }
}
val service1: Service = kodein.with("myTag").instance()
val service2: Service = kodein.with("myTag").instance()
assertThat(service1).isNotSameAs(service2)
Note: we can use Kodein.instance() for configuring transitive dependencies.
4.4. Multiton
Multiton binding is very similar to Factory binding. The only difference is that the same object is returned for the same argument in subsequent calls:
val kodein = Kodein {
bind<Dao>() with singleton { MongoDao() }
bind<Service>() with multiton { tag: String -> Service(instance(), tag) }
}
val service1: Service = kodein.with("myTag").instance()
val service2: Service = kodein.with("myTag").instance()
assertThat(service1).isSameAs(service2)
4.5. Provider
This is a no-arg Factory binding:
val kodein = Kodein {
bind<Dao>() with provider { MongoDao() }
}
val dao1: Dao = kodein.instance()
val dao2: Dao = kodein.instance()
assertThat(dao1).isNotSameAs(dao2)
4.6. Instance
We can register a pre-configured bean instance in the container:
val dao = MongoDao()
val kodein = Kodein {
bind<Dao>() with instance(dao)
}
val fromContainer: Dao = kodein.instance()
assertThat(dao).isSameAs(fromContainer)
4.7. Tagging
We can also register more than one bean of the same type under different tags:
val kodein = Kodein {
bind<Dao>("dao1") with singleton { MongoDao() }
bind<Dao>("dao2") with singleton { MongoDao() }
}
val dao1: Dao = kodein.instance("dao1")
val dao2: Dao = kodein.instance("dao2")
assertThat(dao1).isNotSameAs(dao2)
4.8. Constant
This is syntactic sugar over tagged binding and is assumed to be used for configuration constants — simple types without inheritance:
val kodein = Kodein {
constant("magic") with 42
}
val fromContainer: Int = kodein.instance("magic")
assertThat(fromContainer).isEqualTo(42)
5. Bindings Separation
Kodein allows us to configure beans in separate blocks and combine them.
5.1. Modules
We can group components by particular criteria — for example, all classes related to data persistence — and combine the blocks to build a resulting container:
val jdbcModule = Kodein.Module {
bind<Dao>() with singleton { JdbcDao() }
}
val kodein = Kodein {
import(jdbcModule)
bind<Controller>() with singleton { Controller(instance()) }
bind<Service>() with singleton { Service(instance(), "myService") }
}
val dao: Dao = kodein.instance()
assertThat(dao).isInstanceOf(JdbcDao::class.java)
Note: as modules contain binding rules, target beans are re-created when the same module is used in multiple Kodein instances.
5.2. Composition
We can extend one Kodein instance from another — this allows us to re-use beans:
val persistenceContainer = Kodein {
bind<Dao>() with singleton { MongoDao() }
}
val serviceContainer = Kodein {
extend(persistenceContainer)
bind<Service>() with singleton { Service(instance(), "myService") }
}
val fromPersistence: Dao = persistenceContainer.instance()
val fromService: Dao = serviceContainer.instance()
assertThat(fromPersistence).isSameAs(fromService)
5.3. Overriding
We can override bindings — this can be useful for testing:
class InMemoryDao : Dao
val commonModule = Kodein.Module {
bind<Dao>() with singleton { MongoDao() }
bind<Service>() with singleton { Service(instance(), "myService") }
}
val testContainer = Kodein {
import(commonModule)
bind<Dao>(overrides = true) with singleton { InMemoryDao() }
}
val dao: Dao = testContainer.instance()
assertThat(dao).isInstanceOf(InMemoryDao::class.java)
6. Multi-Bindings
We can configure more than one bean with the same common (super-)type in the container:
val kodein = Kodein {
bind() from setBinding<Dao>()
bind<Dao>().inSet() with singleton { MongoDao() }
bind<Dao>().inSet() with singleton { JdbcDao() }
}
val daos: Set<Dao> = kodein.instance()
assertThat(daos.map {it.javaClass as Class<*>})
.containsOnly(MongoDao::class.java, JdbcDao::class.java)
7. Injector
Our application code was unaware of Kodein in all the examples we used before — it used regular constructor arguments that were provided during the container’s initialization.
However, the framework allows an alternative way to configure dependencies through delegated properties and Injectors:
class Controller2 {
private val injector = KodeinInjector()
val service: Service by injector.instance()
fun injectDependencies(kodein: Kodein) = injector.inject(kodein)
}
val kodein = Kodein {
bind<Dao>() with singleton { MongoDao() }
bind<Service>() with singleton { Service(instance(), "myService") }
}
val controller = Controller2()
controller.injectDependencies(kodein)
assertThat(controller.service).isNotNull
In other words, a domain class defines dependencies through an injector and retrieves them from a given container. Such an approach is useful in specific environments like Android.
8. Using Kodein With Android
In Android, the Kodein container is configured in a custom Application class, and later on, it is bound to the Context instance. All components (activities, fragments, services, broadcast receivers) are assumed to be extended from the utility classes like KodeinActivity and KodeinFragment:
class MyActivity : Activity(), KodeinInjected {
override val injector = KodeinInjector()
val random: Random by instance()
override fun onCreate(savedInstanceState: Bundle?) {
inject(appKodein())
}
}
9. Analysis
In this section, we’ll see how Kodein compares with popular DI frameworks.
9.1. Spring Framework
The Spring Framework is much more feature-rich than Kodein. For example, Spring has a very convenient component-scanning facility. When we mark our classes with particular annotations like @Component, @Service, and @Named, the component scan picks up those classes automatically during container initialization.
Spring also has powerful meta-programming extension points, BeanPostProcessor and BeanFactoryPostProcessor, which might be crucial when adapting a configured application to a particular environment.
Finally, Spring provides some convenient technologies built on top of it, including AOP, Transactions, Test Framework, and many others. If we want to use these, it’s worth sticking with the Spring IoC container.
9.2. Dagger 2
The Dagger 2 framework is not as feature-rich as Spring Framework, but it’s popular in Android development due to its speed (it generates Java code that performs the injection and just executes it in runtime) and size.
Let’s compare the libraries’ method counts and sizes using MethodsCount:
Kodein:Note that the kotlin-stdlib dependency accounts for the bulk of these numbers. When we exclude it, we get 1282 methods and 244 KB DEX size.
Dagger 2:
We can see that the Dagger 2 framework adds far fewer methods and its JAR file is smaller.
Regarding the usage — it’s very similar in that the user code configures dependencies (through Injector in Kodein and JSR-330 annotations in Dagger 2) and later on injects them through a single method call.
However, a key feature of Dagger 2 is that it validates the dependency graph at compile-time, so it won’t allow the application to compile if there is a configuration error.
10. Conclusion
We now know how to use Kodein for dependency injection, what configuration options it provides, and how it compares with a couple of other popular DI frameworks. However, it’s up to you to decide whether to use it in real projects.
As always, the source code for the samples above can be found over on GitHub.