You can find the final code for this blog post on GitHub .
This is Part 1 of a multipart blog series on creating a Kotlin Dependency Injection library. Check out other parts of the series below.- Part 1 - Making a Simple Kotlin Dependency Injection Library
- Part 2 - Optimizing a Kotlin Dependency Injection Library
Why make a Dependency Injection library? Link to heading
In 2016, when my team first started using Kotlin, I wanted to stay away from KAPT for build performance reasons. All other Dependency Injection (DI) libraries for Kotlin / Java relied on reflection, which had runtime performance issues. I learned a lot about Kotlin creating my own DI library using reified types, and thought it’d be fun to recreate a similar library for a blog post, even though there are much better DI options today.
Our Goal Link to heading
This is what we want to create:
class A
class B(val a: A)
class C(
val a: A,
val b: B,
)
val graph = objectGraph {
singleton(A()) // Provided Binding
singleton { B(a = get()) } // Lazy Binding
singleton { C(a = get(), b = get()) } // Lazy Binding
}
val a: A = graph.get()
val b: B = graph.get()
val c: C = graph.get()
Layers in the Code Link to heading
ObjectGraph
- This is the public facade that our users will interact with, and exposes our primary API surface.Binding
- Knows how to resolve a dependency. There are different types of bindings, i.e. in the above example, we use a lazy singleton binding.BindingTable
- Stores all of our bindings, and allows them to be accessed based on what we need.
Binding Link to heading
Binding
s will be a simple interface with a String id
. For our use case, the id will be the fully
qualified name of the class. Each Binding
in a BindingTable
must have a unique id
. In
addition, and most importantly, each binding must know how to resolve / provide the actual
dependency. During dependency resolution, it will have access to the ObjectGraph
so that a
dependency can resolve any other dependencies that may be required.
internal interface Binding {
val id: String
fun resolve(objectGraph: ObjectGraph): Any
}
The Binding
will also be responsible for caching it’s created dependency if necessary.
The simplest form of Binding
would be the singleton binding, that’s already been created and is
being provided when the ObjectGraph
is created.
internal class SingletonBinding<out T : Any>(
override val id: String,
val instance: T,
) : Binding {
override fun resolve(objectGraph: ObjectGraph): Any = instance
}
Binding Table Link to heading
Our BindingTable
is a simple interface that allows us to place and lookup bindings. Our goal is to
define our ObjectGraph
at creation time, place all our bindings into our BindingTable
and from
that point forward, it will be read-only.
internal interface BindingTable : Iterable<Binding> {
fun put(binding: Binding)
operator fun get(className: String): Binding?
}
We have a simple implementation for now, based on a HashMap
. This will be a performance
bottleneck, and it’s something we will improve later (maybe in part 3).
internal fun BindingTable(capacity: Int): BindingTable = HashMapBindingTable(capacity = capacity)
private class HashMapBindingTable(capacity: Int) : BindingTable {
private val map = HashMap<String, Binding>(capacity)
override fun put(binding: Binding) {
if (map.containsKey(binding.id)) {
throw KinjectException("Duplicate binding for class '${binding.id}'")
} else {
map[binding.id] = binding
}
}
override fun get(className: String): Binding? = map[className]
override fun iterator(): Iterator<Binding> = map.values.iterator()
}
ObjectGraph Link to heading
class ObjectGraph private constructor(
private val bindingTable: BindingTable,
) {
class Builder {
private val bindings: MutableList<Binding> = mutableListOf()
fun <T : Any> singleton(
instance: T,
bindType: KClass<*> = instance::class,
) {
bindings += SingletonBinding(bindType.bindingId, instance)
}
inline fun <reified T : Any> singleton(
noinline provider: ObjectGraph.() -> T,
): Unit = singleton(T::class, provider)
fun build(): ObjectGraph {
val bindingTable = BindingTable(bindings.size)
for (binding in bindings) {
bindingTable.put(binding)
}
return ObjectGraph(bindingTable)
}
}
}
private val KClass<*>.bindingId: String
get() = this.qualifiedName ?: error("No qualified name found for '$this'")
Now let’s also provide a builder function with a high-order extension function parameter to make it convenient to use.
inline fun objectGraph(
create: ObjectGraph.Builder.() -> Unit,
): ObjectGraph {
val builder = ObjectGraph.Builder()
builder.create()
return builder.build()
}
Now we can do something like this:
val graph = objectGraph {
singleton(A())
}
We’re creating an ObjectGraph
, loading all of our Binding
s into the BindingTable
, but we can’t
get anything out yet. Let’s fix that.
Resolving Dependencies from the ObjectGraph
Link to heading
If we add the following to our ObjectGraph
, this will allow us to retrieve a dependency by
providing a KClass
.
fun <T : Any> get(clazz: KClass<T>, tag: String? = null): T = internalGet(clazz.bindingId, tag)
private fun <T : Any> internalGet(className: String, tag: String? = null): T {
val binding = bindingTable[className]
?: throw BindingNotFoundException("Binding not found for class '$className'")
return binding.resolve(this) as T
}
Now we can do the following:
val a: A = objectGraph.get(A::class)
We can still do better. By leaning in to Kotlin’s Reified Types, we can add the following function:
inline fun <reified T : Any> get(): T = get(T::class)
By using reified T
we’re telling the Kotlin compiler to resolve T
at compilation time. This
allows us to access the class of T
at runtime with no performance overhead. This also means that
we are required to specify a type when using this function; otherwise the compiler will throw an
error.
val a: A = objectGraph.get() // Works!
val a = objectGraph.get() // Won't compile
Adding a Lazy Singleton Binding Link to heading
We’re almost done! One of the side benefits DI is that most libraries allow you to resolve dependencies lazily. This means you don’t create something until it’s necessary. Let’s add this capability to our library!
First, we need a new Binding. This one is significantly more complicated than the last one we made:
internal class SingletonLazyBinding<T>(
override val id: String,
provider: ObjectGraph.() -> T,
) : Binding {
private var instance: T? = null
private var resolve: (ObjectGraph.() -> T)? = provider
private var isResolving = false
override fun resolve(objectGraph: ObjectGraph): Any {
if (instance == null) {
KinjectPlatform.synchronized(this) {
val resolve = resolve
if (resolve != null) {
if (isResolving) {
throw CyclicDependencyException("A cyclic dependency was found for '$id'")
}
isResolving = true
try {
instance = objectGraph.resolve()
} catch (e: Exception) {
throw KinjectException(message = "Error creating dependency '$id'.", cause = e)
} finally {
this.resolve = null
isResolving = false
}
}
}
}
return instance!!
}
}
Because we’re resolving dependencies lazily, we now have to be worried about the case where dependency A has a reference to dependency B, and B also has a reference to A. This is called a cyclic reference, and would result in an infinite loop while resolving the dependency. We need to detect this and throw an exception if it happens, so we can provide some information about which dependency is having the issue.
This is also a place where we need to be concerned about concurrency. What if two threads try to
resolve the same dependency at the same time? We can easily end up with two of the same “singleton”,
which we obviously want to avoid. We will use a simple synchronized
for this. This is actually the
only place in the code where we are using something that isn’t in the Kotlin common standard
library. Because of that, let’s create an expect / actual function in the KinjectPlatform
class to
expose the synchronized
function, so we can make this entire library multiplatform.
Here are the different platform implementations.
JVM uses the underlying synchronized
function that kotlin exposes:
internal actual object KinjectPlatform {
actual inline fun <R> synchronized(lock: Any, func: () -> R): R {
return kotlin.synchronized(lock, func)
}
}
Apple uses Objective C locking:
internal actual object KinjectPlatform {
actual inline fun <R> synchronized(lock: Any, func: () -> R): R {
objc_sync_enter(lock)
try {
return func()
} finally {
objc_sync_exit(lock)
}
}
}
JS does nothing as it’s single threaded, so we just call the inlined function:
internal actual object KinjectPlatform {
actual inline fun <R> synchronized(lock: Any, func: () -> R): R = func()
}
Now all that’s left is to add the ability to use this new binding from the ObjectGraph.Builder
class:
inline fun <reified T : Any> singleton(
noinline provider: ObjectGraph.() -> T,
): Unit = singleton(T::class, provider)
fun <T : Any> singleton(
clazz: KClass<T>,
provider: ObjectGraph.() -> T,
) {
bindings += SingletonLazyBinding(clazz.bindingId, provider)
}
Adding an ObjectGraph to an ObjectGraph Link to heading
I think the last essential feature is for us to be able to organize our dependencies into “modules”.
To keep things simple, we’ll just use an ObjectGraph
as a module, and allow adding ObjectGraph
s
to other ObjectGraph
s. We can accomplish this with a single function on the ObjectGraph.Builder
.
fun add(objectGraph: ObjectGraph) {
bindings.addAll(objectGraph.bindingTable)
}
That’s it!
Wrapping it Up Link to heading
Let’s write a simple test to show our final feature set, and to check ourselves.
@Test
fun objectGraph_addGraphToGraph_SameBindings() {
val g1 = objectGraph {
singleton(A())
singleton { B(a = get()) }
}
val g2 = objectGraph {
singleton { C(a = get(), b = get()) }
add(g1)
}
val a: A = g2.get()
val b: B = g2.get()
val c: C = g2.get()
assertSame(b.a, a)
assertSame(c.a, a)
assertSame(c.b, b)
}
What’s Next? Link to heading
There are more features we could add:
- Performance Optimization - Right now we’re using a general use HashMap. We can potentially improve performance on certain Android runtimes by creating a specialized hashing implementation just for this use case.
- Factory Bindings - dependencies that are recreated for every injection.
- Tagging a dependency - The ability to have multiple dependencies of the same type by using an identifier to specify which you want to retrieve.
You can find the final code for this blog post on GitHub .
This is Part 1 of a multipart blog series on creating a Kotlin Dependency Injection library. Check out other parts of the series below.