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!