← Blog

Whid — The Origin

Chapter 4: Dependency Injection with Kotlin/Native

29 April 2022

Database is set up, now how do we get stuff to have access to it? This has to be done properly or everything will go to sh*t. I better set up some proper dependency injection before Arthur comes along with one of his awful ideas.
At least he’s working by now. Or rather Paul is while Arthur is being a lazy snail.

TLDR; Issues Addressed

Dependency Injection Basics

“Dependency Injection” is a 25-dollar term for a 5-cent concept.

James Shore

Dependency injection (DI) helps us build software in a decoupled and testable way by changing the way an object obtains its dependencies. Instead of an object being responsible to set up its own dependencies, they are handed to the object at creation or initialization time.

This at its core quite simple concept has some great benefits. For one, we can initialize the same objects with different implementations of an interface. This can be extremely useful when testing where we can replace the production dependencies with mocks that reliably deliver the data we want and reduces the risk of flaky tests.

If you want to learn more about dependency injection, you can read more on James Shore’s Blog.

Setup

The simplest way of doing dependency injection is to just provide the instance of ITimeProvider that you need in the constructor when you create the class that is using it. However, this can be cumbersome in bigger projects where you have a lot of dependencies that you need to provide. That’s where KodeinDI comes in. It can manage dependencies for us and provide them on demand.

Since DI is an established concept and we didn’t want to reinvent the wheel, we decided to go with an existing library for dependency injection.

With our target platform being Kotlin/Native on Windows and MacOS, our choices were quite limited and we ended up with KodeinDI. We used a very helpful Github repository to get an overview of some of the options of Kotlin multiplatform libraries out there. If you know any other useful libraries that are not listed, feel free to add them there.

To get started with KodeinDi, we need to add the library to our gradle build. If you do this in a fresh native build, the dependencies will look something like this:

sourceSets {
    val nativeMain by getting {
        dependencies {
            implementation("org.kodein.di:kodein-di:7.10.0")
        }
    }
    val nativeTest by getting {
        dependencies {
            implementation("org.kodein.di:kodein-di:7.10.0")
            implementation(kotlin("test-common"))
            implementation(kotlin("test-annotations-common"))
        }
    }
}

Using DI for Better Testing

We have installed KodeinDI for our time tracker project, now let’s try it out. First, we are going to build a small piece of code for starting and stopping the time tracking software we are building.

Here is the piece of code to start and stop the tracking. For more infos on what is happening with tracking here, you can check the previous post. For now the important part is that the time tracking service gets its dependencies as constructor parameters.

internal class TimeTrackingService(db: WhidDatabase, private val timeProvider: ITimeProvider) :
    ITimeTrackingService {
    private val timeEntryDataAccess = TimeEntryDataAccess(db)

    override fun startTracking(task: String) {
        timeEntryDataAccess.enterStartingTime(task, timeProvider.getTimeMillis())
    }

    override fun stopTracking(task: String) {
        timeEntryDataAccess.enterEndingTime(task, timeProvider.getTimeMillis())
    }
}

You can find the code for the time provider below. All it currently does is provide the current system time in milliseconds. Note that it implements the interface which was used as the second constructor parameter of the TimeTrackingService. This is important in order to be able to replace it with another ITimeProvider implementation for testing.

class TimeProvider : ITimeProvider {

    override fun getTimeMillis(): Long {
        return Clock.System.now().toEpochMilliseconds()
    }

}

We can now bind this implementation with KodeinDI. Since it’s stateless, we can use a singleton here.

private val database = WhidDatabase(driver)
private val timeProvider = TimeProvider()

override val di = DI {
    bindSingleton { database }
    bindSingleton<ITimeProvider> { timeProvider }
}

With this, we can get the bound ITimeProvider anywhere, where we have the di instance and we can use it to provide dependencies, calling the instance() function when creating objects.

fun getTimeTrackingService(): ITimeTrackingService {
    val timeTrackingService by app.di.newInstance { TimeTrackingService(instance(), instance()) }
    return timeTrackingService
}

Now let’s get back to testing. If we used the TimeProvider in our tests as is, we would encounter the issue that our test results would depend on the system time. We can avoid this by providing a different implementation of the ITimeProvider for our tests, also called mocking. Let’s see how a simple mock looks like!

class MockTimeProvider(private var time: Long) : ITimeProvider {
    val timeProvider = TimeProvider()

    override fun getTimeMillis(): Long {
        return time
    }

    fun setTime(time: Long) {
        this.time = time
    }
}

This implementation allows us to modify time as we need it for our tests and avoid tests failing when testing later on.

Finally, we can use this ITimeProvider implementation in our test cases.

class TimeTrackingServiceTest {
    private val testTimeProvider = MockTimeProvider(0)
    private val testTimeService by app.di.newInstance { TimeTrackingServiceImpl(instance(), testTimeProvider) }

    private val start1: Long = 1000
    private val end1: Long = 2000

    @Test
    fun testStopTracking() {
        assertTrue(compareTasks(emptyList(), testTimeService.getAllTaskEntries()))

        testTimeProvider.setTime(start1)
        testTimeService.startTracking("StartTestString")
        testTimeProvider.setTime(end1)
        testTimeService.stopTracking("StartTestString")

        val expectedTimes = listOf(TimeFrame(1, 1, start1, end1))
        assertTrue(compareTimeFrames(expectedTimes, getTimeTrackingService().getAllTimeEntries()))
    }
}

Now we can write tests without being dependent on changing factors like system time. This is only one of the benefits of dependency injection, though it is in my opinion one of the most important ones.

If you have any feedback, comments or suggestions on how to improve things, feel free to let us know.