📜 ⬆️ ⬇️

Kodein is an interesting alternative to Dagger 2 for dependency injection in Kotlin

Hello, my name is Vladimir, I work as a chief IT engineer at SberTech, in the Digital Business Platform team. Once at dinner, we discussed the pros and cons of Dagger 2 and what we would like to change in our implementation. We are many, and we, respectively, also write a lot of code, so at that time there were already 100,500 methods and half-tons of dex files in our application. Thinking over our brains, we came to the conclusion that we will not be able to write less, but we can reduce the amount of generated code during compilation. So it was decided to look for an alternative to the existing mastodon from Google.



How to choose


We have described a few general requirements for the chosen one that will save us from unnecessary code generation:

1. Generation of dependencies at runtime, not at compilation
')
In our project we have long gone beyond multidex (already three times) and at the moment there are 4 dex-files in the project. An important role in their formation is played by code generation during compilation.

2. Ease of use

Imagine the situation. You need to quickly distribute the architecture, taking into account the application of the dependency injection pattern. You selected several juniors from the team who were assigned to implement dependencies using the framework. For a couple of hours they successfully coped with the task, and the project even started and did not fall in runtime. Quickly and efficiently, everyone is overjoyed.

Experience with Dagger says that a couple of hours is not enough. And even a couple of days. Or even months. Ease of use leads to high performance - and when it comes to dependency injection among 20 million clients, the matter of time cannot be ignored.

3. Functionality no less than Dagger 2

Dagger 2 provides a bunch of interesting implementation possibilities, scopes and other charms of dependency injection. I would not want to lose all this when switching to another framework.

As a result, we stopped at several frameworks:


We discarded the last four options immediately, since they use code generation. Why change the flea? Therefore, a glance fell on Feather, Proton and Kodein. But the first two have not been supported for a long time, and Kodein is released quite often. As a result, we decided to stay on it.

DI or SL?


A small lyrical digression. On Reddit and among android architects, there is a debate about whether this framework is a implementation of the dependency injection pattern, or is it a service locator implementation. Argument of supporters of service locator: so said Jake Warton himself and well justified it. But still the framework developer positions itself as DI. Inside the team, we also argued on this topic, but did not come to a common opinion. I contacted Salomon Brys (framework developer). I quote the answer:

"To be exact, there are a number of differences between lib and a SL:
- A DI lib must understand transient dependencies and order of initialization
- A DI lib must provide scopes (singleton, provider, etc.)
Kodein is a DI lib, for sure. ”

Based on the position of the author of the framework, Kodein is not a service locator, because it more fully meets the requirements of the DI pattern. Personally, I think this is a question of terminology, not what the tool does. If the library meets your needs, then it doesn’t matter what the pattern it implements is called. The main thing is that the work is done without problems.

Implementation and disadvantages


The transition to Kodein became possible when we began to translate our project to a modular architecture - to display all products into separate modules, as independent as possible from each other. This gave us a free hand and allowed us to replace the Dagger 2 in one small module with Kodein. If anything, we could simply roll back to the previous stable version of the module and continue working.

At first, we were a little afraid that Kodein would not work correctly on a Java project, since it was written in Kotlin and uses its “tricks”. But it turned out that Kodein has good support for java-code, and it’s possible to embed dependencies there as easily as on Dagger 2 (and even easier, it will be discussed below).

The transition from Dagger 2 to Kodein was not painful, since the module was rather small, and DI was not very actively used there. The process went quite smoothly, everything started with a half-kick, and the dependencies climbed in and rooted as they were intended. A nice bonus was the absence of problems with kapt, as it was with Dagger 2. Of course, it is better to implement Kodein on a project written in Kotlin, but we experimented, so we had to consider the library from unexpected sides.

All this looks like the perfect silver bullet that will put an end to Google's hegemony and select the title of the best DI framework from Dagger. But Kodein found quite critical flaws during work:

  1. The main and fatty minus is that we can get an error in runtime, not knowing where it came from. This was the problem of the first Dagger, which they got rid of in Dagger 2, going to code generation during compilation. Now we see the same thing in Kodein. The problem is solved by thorough testing and functional coverage by autotests. Moreover, the framework itself pushes us to use its capabilities when writing tests.
  2. If you have an old and large java-project and there is no possibility to connect Kotlin, then Kodein does not suit you. But it is rather a disease of our huge project with years of heritage.
  3. Low popularity in real projects. This leads to the fact that the project is developing much slower than the same Dagger, because developers receive less feedback, fewer repository contributors. Not the fact that we will derive Kodein in production, because now we are still struggling with the drawbacks that we have found.

How to connect


Connecting the framework to the project does not take a lot of time, just simply specify the dependency in the gradle file:

implementation 'com.github.salomonbrys.kodein:kodein:4.1.0' 

Linking to Kotlin:


We declare a Kodein object in which dependencies will be declared.

 val kodein = Kodein {   bind<Message>() with provider { MessageFactory(0, 5) }   bind<DbProvider>() with singleton { SqliteDS.open("path/to/file") } } 

Now more about this: the bind method says that there will be a linking of classes through an inline function. In generics, we specify an interface, the implementation of which we want to inject. After comes the method with, which talks about the method of binding, there are several of them in Kodein, they will be described below, in this case it’s provider and singleton. After that, in curly brackets, we substitute the desired instance of the class that we want to inject.

The binding itself will look like this:

 class Controller(private val kodein: Kodein) {   private val ds: DataSource = kodein.instance() } 

In principle, nothing complicated, you can quickly start working with this framework without knowing the details.

Types of binding


Kodein allows you to inject dependencies in several ways:


 val kodein = Kodein {   bind<Message>() with factory { type: Int -> MessageFactory(type) } } 


 val kodein = Kodein {   bind<Message> with provider { MessageFactory(6) } } 


 val kodein = Kodein {   bind<DBProvider>() with singleton { SqliteDS.open("path/to/file") } } 



 val kodein = Kodein {   bind<Message>() with factory { type: Int -> MessageFactory(type) }   bind<Message>("ImageMessage") with provider { MessageFactory(10) }   bind<Message>("PaymentMessage") with singleton { MessageFactory(20) } } 

There are also opportunities:


Another useful feature is wrapping bundles in weak and soft links. This can be an advantage if you understand that objects should not be long-lived.

An interesting possibility is the binding in local streams. That is, each stream will have its own copy of the injection that we need.

Creating modules


Kodein allows you to store bindings in modules in much the same way as Dagger 2. Example:

 val apiModule = Kodein.Module {   bind<MessengerApi>() with singleton { DefaultMessengerApi() } } 

And then when you initialize the Kodein object, you can specify the desired module:

 val kodein = Kodein {   import(apiModule) } 

Bind override


By default, Kodein does not allow redefining of bindings, as accidental “double” binding can lead to unforeseen consequences and waste time searching for a problem. But this possibility is present. It will be convenient, for example, for testing classes, where you can change the implementation to any Mock. This is done as follows:

 val kodein = Kodein {   bind<MessengerApi>() with singleton { DefaultMessengerApi() }   /* ... */   bind<MessengerApi>(overrides = true) with singleton { CustomMessengerApi() } } 

Imagine that we do not know whether there is already an instance of the type that we want to inject. For such a case, there is a design that allows, in the presence of such an instance, to overload it:

 val kodein = Kodein {   /* ... */   import(testEnvModule, allowOverride = true) } 

There is also a flag that says that it would be preferable to take an already redefined instance. In case there is some “improved” functionality in the “new” instance:

 val testModule = Kodein.Module(allowSilentOverride = true) {   bind<EmailClient>() with singleton { MockEmailClient() } } 

Lazy initialization


Thanks to the delegation of properties from Kotlin, Kodein can use lazy initialization of bindings for almost all types of bindings:

 class Controller(private val kodein: Kodein) {   private val messageFactory: (Int) -> Dice by kodein.lazy.factory()   private val dbProvider: DataSource by kodein.lazy.instance()   private val randomProvider: () -> Random by kodein.lazy.provider()   private val answerConstant: String by kodein.lazy.instance("answer") } 

How classes are generated in runtime


Generation of classes occurs due to embedding functions. This allows embedding in the code when compiling. That is, the compiler simply inserts a piece of the necessary code into the place where the built-in function will be declared. The generation of classes in Kodein follows the same principle. The choice of the necessary types for creation of copies happens at the expense of reified generics. You can read more on the links at the end of the article.

Kodein on java-projects


And what about those who have projects written not in Kotlin, but in Java? Kodein provides the same functionality and good old Java, but with some limitations. The framework partially supports the JSR-330 specification, specifically the Inject annotation, which is familiar to anyone who has ever encountered DI in Java.

There are some disadvantages here. Injections are not made through inline functions and generics, but with the help of reflection. Accordingly, there will be a loss in performance due to reflection and multilingual compilation. In addition, the object with the bindings must be written on Kotlin - if the conditions indicate the use of exclusively Java, then, alas, Kodein will not work for you.

To use Kodein on java-projects, you need to take a few steps:

  1. Connect JxInjector in gradle:

 compile 'com.github.salomonbrys.kodein:kodein-jxinject:4.1.0' 

2. Add the jxIntector module to the Kodein object:

 val kodein = Kodein {   import(jxInjectorModule)   /* Other bindings */ 

3. Perform binding. It can be done in two ways. The first way is classic. Just write the inject annotation on the required field, write the method - and everything will work the same as on Dagger.

  public class MyJavaController {   @Inject   public MyJavaController(Connection connection, FileSystem fs) {       /* ... */   }   /* ... */ } 

The second way is using specific Kodein objects:

 MyJavaController controller = Jx.of(kodein).newInstance(MyJavaController.class); 

or so:

 MyJavaController controller = new MyJavaController(); Jx.of(kodein).inject(controller); 

Conclusion


A simple and lightweight DI framework that allows you to understand the basics literally overnight and begin to implement it in your project, and even written in Kotlin and proven in production? It sounds too good, but with Kodein this has become true.

This framework is not a 100% alternative to Dagger 2, but it can be used on fat projects with lots of code, where even Mr. Proper could not do it, where additional code generation can result in a new dex file. But do not assume that for small projects Kodein will be bad. It greatly simplifies dependency injection without requiring knowledge of the mechanisms specific to Dagger 2.

Kodein also fits Kotlin very well and feels pretty good on such projects. Here, at least, the code generation of classes is very interesting - this is a deviation from the approach with reflection in favor of the opportunities offered by Kotiln. Unfortunately, all the code written in the framework of this experiment, we have not yet brought into production, since there is no 100% certainty that the framework will not shoot us in the leg. The study is ongoing.

useful links


Source: https://habr.com/ru/post/347208/


All Articles