DI.kt, One of the First Kotlin Multiplatform DI Libraries

Serge Shustoff
Nov 19 · 5 min read

I’m here to present a new DI library for Kotlin multiplatform — DI.kt.

You may be asking: Why another library? For a long time, there were no real DI libraries for Kotlin Multiplatform. The existing libraries were all service locators (koin, kodein, popkorn) without compile-time dependency graph validation. Compile-time safety is one of the most important features in DI frameworks and we needed some options to choose from, which is where DI.kt comes in. This library is much simpler than dagger-like solutions — there are no multibindings or other complex concepts that are hard to understand and easy to misuse.

Disclaimer: The library is currently in alpha because it uses an undocumented compiler plugin API.

What does it do?

The compiler plugin generates code for creating entities. That’s all we need from a simple DI library.

How does it work?

DI.kt uses undocumented compiler plugin API and, instead of generating source files, it adds all needed code during IR generation. This leads to some limitations, but it also severely reduces the library’s impact on project compilation time, because, without generating source files, there is no need for additional compilation cycles.

How is it different from other libraries?

When we forget to provide some dependency or add cyclic dependency, we get a compilation error. With DI.kt, there’s no need for a test to see if the dependency graph was built correctly, and no crashes in compile-time because the dependency we wanted requires something we didn’t provide.

DI.kt is simple and concise. All DI-related code and annotations go to the modules in DI.kt. Six annotations cover everything we need.

So, how do I use it?

That’s relatively simple. First, we need to add a plugin to our Gradle project:

Now let’s try it in a hypothetical situation. You can easily adapt this to your real project instead.

Let’s say we have some DoImportantWorkUsecase that requires MyRepository to do its job. We want DoImportantWorkUsecase to be created at some point without knowing all its dependencies. DoImportantWorkUsecase have MyRepository as a constructor parameter:

That’s a classic case of constructor injection. Now, we need a place where our use case can be created without us explicitly passing that repository as a parameter. This means we need to hide the details somewhere, like in MyModule. Modules are common for DI frameworks, though they’re implemented differently. Here we only need a class:

That’s how the library works — we declare a function and leave it for the library to generate the function’s body. There is one problem — our IDE doesn’t know that the function’s body is generated by the library and it shows us an error. We need to install an Idea Plugin to remove this error. Now we can continue.

If we try to compile this code, we will see a compilation error on the doImportantWorkUsecase function: “Can’t resolve dependency MyRepository”. That’s because our module doesn’t know where to get or how to create MyRepository.

Let’s fix that. We just need to tell our module that it’s allowed to create MyRepository and store it as a singleton (kind of — it’s bound to the module’s instance, so it’s technically not a real singleton):

We have made it private because we don’t need to expose low-level details to module users, but it could be public if we wanted to.

Now we only need to provide MyRepository’s dependencies. I assume it’s a database and some kind of network client:

Let’s say we pass the database from outside, so it’s provided where our module is created. We just add a field to the module like this:

DI.kt will be able to use this field in generated functions. This means only the http client is missing. We can create it inside the module in lazy property:

Finally, our code compiles without errors. The use case has been created in the module with everything needed to be provided under the hood when we call myModule.doImportantWorkUsecase().

The final version of the module looks like this:

It does look a bit boilerplate, but when we need to provide the same dependencies in multiple use cases, presenters, or view models, we end up adding one function without a body for each new use case and then using this function in our code.

Notice that DoImportantWorkUsecase class doesn’t need any annotations. That’s where DI.kt is different from dagger-like DI frameworks — we don’t need to pollute our code with DI details. It is also a trade-off — it makes things a bit more difficult because we need to specify in the module what we can create there. There is an annotation that allows us to omit some of that boilerplate code — we can annotate our module with @UseConstructors(MyRepository::class) and remove the myRepository function. But in this case, the repository is’t a singleton anymore.

We can also move HttpClient creation to another module and mark MyModule with another annotation telling it to use properties and functions from that module as dependencies:

The approach to modules here is similar to one from Koin or Toothpick rather than Dagger. For Dagger users, MyModule would be a Component and HttpModule — a Module. With DI.kt, it’s simplified — we don’t need too many different entities. Instead, a module can provide dependency for both a user and for another module.

Assisted injection is another thing worth mentioning — what if we wanted to pass some very important parameter to DoImportantWorkUsecase’s constructor from code that calls module.doImportantWorkUsecase()? We can simply add it as parameter to our function:

In DI.kt, dependency can be provided from function arguments, module properties and functions, from other available modules, or simply created by the primary constructor.

I originally mentioned six annotations. The last two are @Provide and @ProvideSingle. They are similar to @Create and @CreateSingle, but instead of calling the primary constructor they provide dependency from nested modules. For example, if we wanted a user to be able to get HttpClient from MyModule, we would simply add @Provide fun httpClient(): HttpClient in MyModule and DI.kt would generate method’s body that returns httpModule.client. An important point to note here is that @Create always generates a method that calls a constructor, but it’s not always what we need.

Pros and cons

In our line of work, any tool always has its pros and cons, DI.kt isn’t an exception.

Pros

  • Easy to migrate from manual dependency injection
  • The dependency graph is validated in compile-time — if it builds, it works
  • All DI-related stuff is placed in DI modules and doesn’t pollute other code
  • Full Kotlin Multiplatform support

Cons

  • Requires more boilerplate code then some alternatives
  • Requires IDE plugin or will show a lot of errors in IDE
  • Can’t see generated code or find where dependency is used (for now)
  • It’s an alpha version

That’s all for now. Please give the library a try and thanks for reading.

Wrike TechClub

We post articles, run meetups, and take part in conferences all over the world.

Wrike TechClub

Each day, Wrikers from different offices share their knowledge and experience with communities through different platforms. Learn more about design in Wrike: https://medium.com/wrike-design Customer Success & Support in Wrike: https://medium.com/wrikecxblog

Serge Shustoff

Written by

Wrike TechClub

Each day, Wrikers from different offices share their knowledge and experience with communities through different platforms. Learn more about design in Wrike: https://medium.com/wrike-design Customer Success & Support in Wrike: https://medium.com/wrikecxblog