A few weeks back we implemented a very basic dependency injection container in Kotlin using reflection (see
here). But here's something cool about Kotlin: it's powerful and flexible enough to allow for a pretty solid dependency injection experience without even pulling out reflection or annotation processing. Check this out:
fun main() {
val dep4 = Injector.dep4
println(dep4)
}
object Injector {
val dep4 by lazy { Dep4() }
val dep1 by lazy { Dep1() }
val dep3 by lazy { Dep3() }
val dep2 by lazy { Dep2() }
}
class Dep1
data class Dep2(
val dep1: Dep1 = Injector.dep1)
data class Dep3(
val dep1: Dep1 = Injector.dep1,
val dep2: Dep2 = Injector.dep2)
data class Dep4(
val dep3: Dep3 = Injector.dep3)
And we could take this one step further, and allow for mocks to be injected for integration tests:
// Here's the implementation
fun main() = run()
// Running this main method will print this to the console:
// Dep4(dep3=Dep3(dep1=Dep1@610694f1, dep2=Dep2(dep1=Dep1@610694f1)))
fun run(injectorOverride: Injector? = null) {
injectorOverride?.let {
injector = it
}
val dep4 = inject().dep4
println(dep4)
}
open class Injector {
open val dep4 by lazy { Dep4() }
open val dep1 by lazy { Dep1() }
open val dep3 by lazy { Dep3() }
open val dep2 by lazy { Dep2() }
}
private lateinit var injector: Injector
fun inject(): Injector {
if (!::injector.isInitialized) {
injector = Injector()
}
return injector
}
class Dep1
data class Dep2(
val dep1: Dep1 = inject().dep1)
data class Dep3(
val dep1: Dep1 = inject().dep1,
val dep2: Dep2 = inject().dep2)
data class Dep4(
val dep3: Dep3 = inject().dep3)
// Here's some hypothetical test code
import io.mockk.mockk
fun main() = run(TestInjector())
// Running this main method will print this to the console:
// Dep4(dep3=Dep3(dep1=Dep1@72c28d64, dep2=Dep2(#1)))
class TestInjector: Injector() {
override val dep2 by lazy { mockk() }
}
This could of course be further improved upon, but it shows that in not all that many lines of code, we've got a pretty solid dependency injection setup.