Kapsule — minimalist dependency injection for Kotlin

Introductory articles are all the same: here’s the new shiny (used as a noun), it’s really cool, please use it. Admittedly, this post is about my new library called Kapsule, but I want to first discuss the state of dependency injection in Kotlin and hence explain what drove me to create this library.

Current state

Here’s an obvious statement: dependency injection is a pattern, not a library. This means you can technically do it without any libraries at all. But if you attempt that, much of the code facilitating the injection will be repeated between your projects and you’ll decide to extract it. So now you have a library…

In the Kotlin world, we’ve come to accept Dagger and, to a lesser extent, Kodein as the primary choices for such facilitation:

  • Dagger is a Java library based on annotation-driven code generation. Dependency providers are organised into modules and these modules are then paired with components to perform the actual injection.
  • Kodein uses a similar approach, except it’s written in Kotlin and doesn’t use annotations. Instead it uses delegated properties and does away with the concept of components.

Both libraries are designed to simplify the injection process, however they themselves are complex structures and that encourages users to build their dependencies around these structures, instead of making the libraries work with their dependencies. This is where the issues start.

Issues

To avoid picking on specific libraries too much, I’ll focus on the traits and implementation details that I disagree with. Most of them apply to multiple libraries anyway.

Untraceable magic

Most dependency injection libraries seem to focus too much on seamlessness, which ends up looking borderline magical and hard to trace in large projects.

“Isn’t this the point of dependency injection?” you may ask. Not really. The point is to decouple your application by separating the code that provides the dependency from the code that uses it. Concealing the injection process is completely optional.

Why does that matter? Because it affects readability: when you’re looking at unfamiliar (or forgotten) code with many injected fields, you can’t click through the references in your IDE like you would with regular method calls. Instead, you have to find the corresponding module and look for that type (and string identifier for named dependencies) to understand how it’s being provided.

Other types of libraries experience it too; remember the event bus? Developers would inherit a codebase with countless subscription methods all waiting for events to emerge from the depths of the application architecture. No wonder we (as an industry) largely replaced it with Reactive Extensions.

Extending classes

As a general rule, any library forcing you to extend their base classes to add functionality has issues. Don’t get me wrong, inheritance didn’t suddenly become taboo, so long as the class is actually a more specific type of its parent, not just a type that supports library X.

Beyond the ideological problems with this approach, there is the practical nightmare of clashes. Android developers would remember trying to create an activity that supported RoboGuice and ActionBarSherlock, and failing because both required you to extend their base activities. Solution? RoboSherlockActivity—a library that introduced another base activity that supported both! What a mess…

No private finals

This one is a petty Kotlin-specific gripe: annotation-based injection doesn’t allow for private or final fields. While annotated @Inject, it has to be a public lateinit var, even if it’s only used privately and it never changes.

Not critical (which is why it’s second to last on this list), but it sure is annoying. You know you shouldn’t be re-assigning it or using it from outside the class, but will the next person reading your code? Perhaps you could document it… Oh, wait, that’s what private and val are for!

Heft

Lastly, there’s the file size and method count. Admittedly, the method count is only crucial for Android, but most platforms will have you packaging up all the artifacts into an installable distribution, so it still matters.

To me it feels excessive for a dependency injection library to contain more than, say, a thousand methods. Kotlin runtime is just over a thousand. Think about that, the whole runtime of a programming language is smaller than a dependency injection library.

Path to improvement

After complaining about excessively complex structures, I decided to experiment with writing a minimalist prototype that would still satisfy the basic dependency injection requirements for most of my projects. The result eventually became known as Kapsule.

The library has detailed documentation on its GitHub page, so I won’t repeat all of it here, but let’s look at a basic Android example.

Consider a fragment that requires some dependency dataManager.

class HomeFragment : Fragment() {
private val dataManager = ???
}

Let’s say this manager is available from a dependency injection module initialised and stored in the application context. Although the value should only be assigned once (because val), that can only be done in the onAttach() method, because we have no access to the context before then.

Here’s how Kapsule can help:

class HomeFragment : Fragment, Injects<AppModule> {
private val dataManager by required { dataManager }

override fun onAttach(activity: Activity) {
super.onAttach(activity)
inject(DemoApplication.appModule(activity))
}
}

What’s changed?

  1. HomeFragment now implements the interface Injects<AppModule> to indicate which module it depends on and provide some convenience methods for injection.
  2. dataManager is now a delegated property by required(), a convenience method on Injects, which takes a function from the module to the field type, i.e. AppModule.() -> DataManager. This means that all dependencies are named and tied together by direct reference, rather than string identifiers and types. Note: As the name suggests, required() only supports non-null types, for nullable types there is optional().
  3. Finally, we obtain the module instance from the application context and inject values into the delegated properties with inject(), another convenience method on Injects.

Finally, here’s the simplest implementation of AppModule:

class AppModule {
val dataManager = SomeDataManager()
}

That’s all there is to the framework. It only facilitates the injection, everything else is up to you:

  • AppModule can be any class or interface you like, i.e it doesn’t have to extend anything related to Kapsule! It can be one class (as above) or an interface with multiple implementations (e.g. main and test); it can also be a combination of multiple modules via delegation.
  • Accessing fields inside the required/optional functions can also be done in any way you like: as a property (as above) or as a method (e.g. required { createDataManager() }).
  • You could even use your existing Dagger modules and mix Kapsule in, instead of ripping everything out and converting (like you mixed Kotlin in with Java when it all started)

Although implementing Injects<AppModule> doesn’t limit you from extending/implementing other things, depending on your preference, you may choose to manually create an instance of Kapsule<Module> and use that for injection.

class HomeFragment : Fragment {
private val kap = Kapsule<AppModule>()
private val dataManager by kap.required { dataManager }

override fun onAttach(activity: Activity) {
super.onAttach(activity)
kap.inject(DemoApplication.appModule(activity))
}
}

That covers the most basic use case and hopefully gives you some taste for what the library is used for. For documentation and samples (including a detailed Android one), see the GitHub page.

Only the beginning

In its current state, Kapsule covers all the issues that I discussed in this article and is generally stable. However, it leaves a lot to the developer and while this is intentional to keep the structure light, it may not be what everyone wants.

Therefore, there are plans to add more features in the form of separate modules. To keep the ‘heft’ down, kapsule-core is intended to remain tiny (as of version 0.2, it’s 77 methods and 12 KB) and leave it to other artifacts to provide extended functionality in a modular way, ensuring that you only include what you need.

Having said that, the future of Kapsule depends on its usage. If after a while I’m still the only person using it, then active development will be limited to only the features that I need. However, if the interest picks up, then new features will depend on what the library’s user base actually needs.

Here are some potential features that I’m considering:

  • Helpers for module structure
  • Module retention framework (managing memory and scopes)
  • Modules for platform-specific dependencies: Android, Vert.x, etc.
  • Integration with mocking frameworks for unit testing

For now those are just ideas that you are welcome to give feedback on, along with the current state of the library, via the issue tracker.

Thanks for reading and hopefully Kapsule can be as useful for your projects as it is for mine.