feature

I was experimenting with Dependency Injection for an IntelliJ plugin and my first thought was, “A plugin is just a JVM program, can’t we use Dagger or Koin?” So, I started experimenting with Koin, as it seemed easier for such projects.

I added Koin to the dependencies as I do for any Gradle project, and it worked fine. However, I soon saw the challenges of this approach. Like any DI, you need a starting point to initiate the framework, and for Koin, this means calling:

startKoin{...}

But what could be the starting point for such a plugin? Where exactly can we run this code? After going through a lot of deprecated code—like, a LOT—and some internal APIs, I found a listener called AppLifecycleListener.

So, I created a class that extends it and overrode appFrameCreated, which only gets called when the application starts, meaning when the IDE first loads.

class ScreenverseApp : AppLifecycleListener {

    override fun appFrameCreated(commandLineArgs: MutableList<String>) {
        super.appFrameCreated(commandLineArgs)

        startKoin {
            modules(
                analyzerModule,
            )
        }
    }
}

This seemed too good to be true after losing my soul in the deprecated APIs.

But after some investigation, I found that there’s no single starting point for any plugin, and for the appFrameCreated, there are still some extension points that can be initiated before it. So, it’s not guaranteed that the DI will be started before I run the plugin. This can be dangerous and error-prone.

So, I started thinking this might actually not be possible to add DI to plugins, but then I remembered something!!!

Services!! IntelliJ plugin system offers something called services that use PicoContainer, which is actually a DI!

So, the plugin already has a built-in approach to dependency injection.

Let’s dive more into it and how to set it up.

Let’s take this setup for example:

interface Service1
class Service1Impl: Service1

interface Service2
class Service2Impl(
    private val service1: Service1,
): Service2

We have two interfaces and two implementations, and Service2Impl uses Service1. This means when I try to create Service2, I should get a binding from DI that gets me Service2Impl and injects it with Service1Impl.

Can this be achieved using services in the plugin? The answer is yes.

Here’s how you do it. Go to plugin.xml and register the services:

 <applicationService
                serviceInterface="com.example.Service1"
                serviceImplementation="com.example.Service1Impl"/>

 <applicationService
                serviceInterface="com.example.Service2"
                serviceImplementation="com.example.Service2Impl"/>               

Using ApplicationService means this will have a scope of the application. You can switch to ProjectService if you need that scope, but for DI, I think the app scope is better.

Now let’s see how to get the Service2Impl:

val service2 = service<Service2>() // Service2Impl

Works as intended!