Sharp Dagger or Round Koin?
While I was prepping myself to talk about dependency injection and Dagger last year at a tech conference in Yangon, I had the opportunity to research about Koin. Back then, Kotlin was growing really fast since it was just announced to be supported as first class language of Android. So, I thought to myself, “Will Koin replace Dagger in the future?”. I don’t have a definite yes/no answer to that yet, but I can share with you what I’ve learned and you can come to conclusion of your own.
As a way to learn Koin, I started migrating a small projects (with 4 screens) from Dagger to Koin. It only took me around 5 or 6 hours at most to finish migrating all my tests and classes. Sadly, I can’t share the source code due to the nature of the project, but I can share you snippets from it.
Documentation and Support
The documentation is way better compared to Dagger’s (Hey, there’s no thermosiphon involved). It revolves around our daily presenter and repository pattern, making it less of a hassle to understand the process. Koin’s complexity isn’t as large as Dagger which may also contribute to a much easier learning curve. Nonetheless, when I had some issues tinkering with it and tried to google it, I found out there wasn’t much community yet. I couldn’t find the related issues on Stackoverflow, or even in blogs. Luckily, I managed to solve it by reading through the code.
Dagger performs code generation through annotations, whereas Koin doesn’t do any code generation. A good thing about this is you don’t need to build and wait till it finishes to start using DaggerAndroidComponent to inject in your Application class.
No proxy, no code generation, no introspection. Just DSL and functional Kotlin magic!
Koin effectively reduce the amount of dependencies you have to add in your gradle. In my case, it reduces half alone in my app-ui module (from 6 to 3). I haven’t test on compile timing yet, so I can’t be sure on whether one compiles faster or not compared to the other.
One thing I got frustrated with DaggerAndroid is the amount of setup time it takes for it to actually start using it in the project. I mean check out this amount of code.
All of these in Koin can be replaced with just one module variable. Yes, you read it right, just one module variable.
There you go, it’s done. Start it from your Application with startKoin
and you’re all set. It just works. Did you notice that I didn’t provide ApplicationContext
? Well, that’s because it’s automatically provided when you run startKoin
in your Application. Are you interested in Koin
now? Great, let’s dive into it then. A lot of what I wrote will be on the documentation, but if you want my take on stuffs, read along.
Provides
In Koin, applicationContext
creates a module. Note that this does not mean the context from Android; it stands for KoinContext
, which koin uses internally to bind your components. bean
is the syntax for providing a dependency. In Koin, everything’s provided as singleton by default. You can override this settings by using factory
instead of bean
or by providing isSingleton=false
in the parameter of bean()
You can also use Koin’s properties to provide static values from a property file you created. I haven’t found a use case for this yet, but if you found one, please share with me.
Inject
By default, constructor injections will be resolved automatically. If you need to inject within your module, get()
(or getProperty()
for properties) will try to resolve the dependency for you. In cases where you can’t use constructor injection. You can use inject
delegation. A pro in this scenario is that we no longer need to use lateinit var
anymore. This makes sure that the instance you provided through DI cannot be changed from the code after it has been created.
val errorMessageFactory: ErrorMessageFactory by inject()
In DaggerAndroid, I had to write the same code twice in the constructor function of both ViewModel and ViewModelFactory. So whenever a parameter change in ViewModel, the factroy has to be changed too. When the project get bigs, it becomes a chaotic mess of ViewModelFactory and ViewModel.
In Koin, we can inject the ViewModel directly into our Activity/Fragment by using viewModel delegation. This is a huge plus for me because it significantly cut down my codes compared to Dagger.
Testing
koin-test
provides functionality to inject components into a JUnit Test. First, you have to extends your class from KoinTest
. Then, in your setUp
function, run startKoin()
with your test module. However, if you’re like me and use constructor injections, it will be a rare case for you to inject them with koin. Nevertheless, it’s a great use case if you need to inject into a class where constructor injection isn’t possible. You can check out official docs for sample code.
For instrumentation test, you would have to write a test module with mocked instances. Make sure you’re using bean
for these since you would want a singleton instance for you to able to mock. Next, start your Koin as you would normally in TestApplication. For test to access the mocked instance, you would also require to provide the KoinContext
with a static function. Check out the code below!
Scoping
Koin provides a very neat way to provide scoping. It uses context{}
for scoping.context{}
defines a sub context within the root context. By default, context isolation is turned off. To explicitly allow it, you need to call Koin.useContextIsolation=true
before startKoin()
.
As explained in the docs, in this case,
- beans from
B
can see beans fromA
andRoot
- beans from
A
can see beans fromRoot
- beans from
C
can see beans fromRoot
Note that you have to release your context manually by calling releaseContext(CONTEXT_NAME)
to destroy the instances.
In my case, I scope the viewmodel instances for each Activity/Fragment in the module, and then I release the context in onDestroy
. This might not be the best solution yet, but it works perfect in my case.
There are still more features inside Koin, you should definitely check out the docs!
Downside
Kotin is cool and all but it also has its own falls. First I noticed is that I had to provide get() for all parameters in the ViewModel. It doesn’t just create the instance with the dependency provided. For example, I might have the following code
class NewsRepo constructor(
networkManager: NetworkManager
)
I can’t just instantiate the NewsListingViewModel with the following code. It doesn’t construct the NewsRepo with the provided networkManager.
bean<NetworkManager> {
NetworkManagerImpl()
}viewModel {
NewsListingViewModel(get())
}
You either have to explicitly provide the NewsRepo with a bean definition or you can construct it in the parameter of the ViewModel.
bean<NetworkManager> {
NetworkManagerImpl()
}bean {
NewsRepo(get())
}viewModel {
NewsListingViewModel(get())
}
This is a small boilerplate but it’s a lot better than having to provide a ViewModelFactory for each ViewModel.
The next drawback is that there’s no dependency injection error at compile time. In Dagger, it will tell you that you forgot to provide an instance if it can’t satisfy the injection. In Koin, you only know the problem during runtime. This is potentially a huge big no, but not to worry much, Koin provides dry run where you can run the test to make sure all components are satisfied correctly.
The last thing is as I mentioned about earlier, there isn’t a big community yet, so it might be hard to find some edge case solutions.
Like everything in life, there’s always ying and yang. Whether Koin fits for your project is up to you to decide. I think I’ll be sticking to Koin for a while and really try to use it on a full production app to see if it kicks off really well. Leave your thoughts, if you have experimented around with it.
PS : The developer recommends a holder pattern for injection inside activities/fragments
Update : While I first naively thought Koin as DI, some people have pointed out that it’s a service locator rather than a dependency injector. I haven’t read up on this subject yet but here’s a quote from Martin Fowler.
The fundamental choice is between Service Locator and Dependency Injection. The first point is that both implementations provide the fundamental decoupling that’s missing in the naive example — in both cases application code is independent of the concrete implementation of the service interface. The important difference between the two patterns is about how that implementation is provided to the application class. With service locator the application class asks for it explicitly by a message to the locator. With injection there is no explicit request, the service appears in the application class — hence the inversion of control.
Inversion of control is a common feature of frameworks, but it’s something that comes at a price. It tends to be hard to understand and leads to problems when you are trying to debug. So on the whole I prefer to avoid it unless I need it. This isn’t to say it’s a bad thing, just that I think it needs to justify itself over the more straightforward alternative.
The key difference is that with a Service Locator every user of a service has a dependency to the locator. The locator can hide dependencies to other implementations, but you do need to see the locator. So the decision between locator and injector depends on whether that dependency is a problem (…)
Martin Fowler — Inversion of Control Containers and the Dependency Injection pattern