Back
May 9, 2022

Dependency injection with Koin for your Kotlin Minecraft plugin

Choosing Koin

I'm not suggesting you add dependency injection to a small or new project, but libraries and tools are there for you when you eventually run into the problems that they aim to solve. During development of a Paper plugin, my dependencies and services were growing, so I looked into the dependency injection options for Kotlin. I found Koin, and it looked simple to set up and reduces a lot of boilerplate, particularly around passing in dependencies into all of my constructors.

Setup

Add Koin to your dependencies, and add Maven Central to your repositories if you don't have it already.

repositories {
    mavenCentral()
}

dependencies {
    implementation("io.insert-koin:koin-core:3.1.5")
}

Create your module and start Koin in your onEnable method in your plugin main class. Here I have registered the plugin class and the logger as singletons. I can now reference these in my classes, such as listeners and other services.

class ExamplePlugin : JavaPlugin() {
    override fun onEnable() {
        val exampleModule = module {
            single { this@KrimePlugin }
            single { this@KrimePlugin.logger }
        }

        startKoin {
            modules(exampleModule)
        }
    }
}

That's it!

Injecting dependencies

If you are creating a class that other classes will depend on, it's best to use constructor injection. It means there's less third-party/Koin-specific code in your codebase, which is always a win.

Here I create a service with a simple function. It takes 2 dependencies, the plugin, to schedule a task on the server, and the logger, to log to the console. When this service is created by Koin, it'll get the 2 dependencies from the module graph (defined earlier on), and pass them to your constructor.

class ExampleService(private val plugin: ExamplePlugin, private val logger: Logger) {
    fun delayedLog(player: Player) {
        plugin.server.scheduler.runTaskLater(plugin, Consumer { logger.info("Hello from ${player.name}") }, 20)
    }
}

Now I want to inject this service into my event listener. As this won't be registered as part of our modules block, I need to imlpement the KoinComponent interface and use inject to retrieve the dependency.

Here I inject ExampleService, which itself has 2 dependencies. The service will created once by Koin, and shared amongst all consumers, such as this listener.

You can inject any classes you have set up in your Koin modules using by inject().

import org.koin.core.component.KoinComponent
import org.koin.core.component.inject

class ExampleListener : Listener, KoinComponent {

    private val exampleService: ExampleService by inject()

    @EventHandler
    fun onPlayerInteract(event: PlayerInteractEvent) {
        exampleService.delayedLog(event.player)
    }
}

The last thing I need to do is update my plugin main class with these new classes.

  1. Register ExampleService as a singleton dependency
  2. Register the ExampleListener to the server
class ExamplePlugin : JavaPlugin() {
    override fun onEnable() {
        val exampleModule = module {
            single { this@KrimePlugin }
            single { this@KrimePlugin.logger }
            singleOf(::ExampleService) // 1
        }

        startKoin {
            modules(exampleModule)
        }

        server.pluginManager.registerEvents(ExampleListener(), this) // 2
    }
}

You can also inject into your plugin class (1) as well. inject is lazily evaluated, so you need to use your dependency after startKoin runs.

class ExamplePlugin : JavaPlugin(), KoinComponent {

    private val exampleService: ExampleService by inject() // 1

    override fun onEnable() {
        val exampleModule = module {
            single { this@KrimePlugin }
            single { this@KrimePlugin.logger }
            single { ExampleService() }
        }

        startKoin {
            modules(exampleModule)
        }

        server.pluginManager.registerEvents(ExampleListener(), this)

        exampleService.delayedLog("Hello from my plugin") // Let's pretend a function exists that takes a string
    }
}

But why?

There are a variety of reasons to use dependency injection. By not relying on static calls inside your classes, it makes it much easier to mock dependencies when writing tests. For example, we can mock ExampleService and write unit tests for our ExampleListener class. There is a Koin testing library which allows you to set up mocks in your tests.

You don't need Koin to do dependency injection, in fact I would advice against it for smaller projects. You can do manual dependency injection very easily using manual constructor injection.

class ExamplePlugin : JavaPlugin() {
    override fun onEnable() {
        val exampleService = ExampleService(plugin, logger)

        server.pluginManager.registerEvents(ExampleListener(exampleService), this)

        exampleService.delayedLog("Hello from my plugin") // Let's pretend a function exists that takes a string
    }
}

class ExampleService(
    private val plugin: ExamplePlugin,
    private val logger: Logger,
) {
    fun delayedLog(message: String) {
        plugin.server.scheduler.runTaskLater(plugin, Consumer { logger.info(message) }, 20)
    }
}

class ExampleListener(private val exampleService: ExampleService) : Listener {
    @EventHandler
    fun onPlayerInteract(event: PlayerInteractEvent) {
        exampleService.delayedLog("Hello from ${event.player.name}")
    }
}

Dependency injection makes your code clearer, rather than relying on global state. It also makes testing easier, allowing you to easily mock dependencies. This is a well-researched topic, so I'd encourage you to search online if you want to learn more!

Discord username
quzmo