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.
  1. Part 1 - Making a Simple Kotlin Dependency Injection Library
  2. 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

Bindings 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 Bindings 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 ObjectGraphs to other ObjectGraphs. 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.
  1. Part 1 - Making a Simple Kotlin Dependency Injection Library
  2. Part 2 - Optimizing a Kotlin Dependency Injection Library