There is a very thin lining of difference between Service Locator (in short SL) and Dependency Injection (in short DI). Most people get confused with these terms, even I was confused with them for so long. Even more so for Android Developers, most of us often get in dilemma whether to use Koin or Dagger in their project. In this blog, I'll try to answer the questions regarding SL and DI, and will try to define them using simple terms, and will also compare Dagger and Koin for Android environment so that you can make the decision your self. But if you ask me for a one-sentence answer on whether to use Dagger or Koin in your project, my answer will be "it matters".
The goal of this blog isn't to show how to use Koin or Dagger, rather the goal is to clear the area around the DI and SL concepts, and comparing and differentiating Dagger and Koin so that it's easy for you to decide which one is best suited for your project.
Inversion of Control (IoC)
First thing first, all of this DI and SL comes from the IoC principle, so let us understand the IoC principle first. IoC or Inversion of Control or Dependency Inversion is the D of SOLID principles. We'll not go to all the bookish definitions, what IoC says is, in short, a class that is using a dependency shouldn't create it, rather than should be provided the dependency with. So, instead of creating the dependant class first, and then let the dependant class create the dependency, the dependency class should be created and provided to the dependant by a third party.
As we discussed a class shouldn't create its own dependencies, the question comes what exactly is a dependency? Let us take some real-life examples to answer that. If you're following MVVM architecture pattern in Android, you might access the ViewModel class from Activity / Fragment / View. So, your view (Activity / Fragment / View) class depends on that ViewModel instance, so the ViewModel instance is dependency and the view (Activity or Fragment or View) class is the dependant.
What's Dependency Injection (DI)?
Dependency Injection is a pattern/principle that provides a way to incorporate IoC. It says the dependency should be injected to the dependant. You can take the word 'inject` to its literal meaning here, much like the injections in real life, in DI, the dependant doesn't know how the dependency would come or from where, it just uses the dependency when it needs, and a third party injects that dependency right after the dependant instance is created.
In DI, it's important that a class should never know where the dependency comes from, or should never request for the dependency itself (if you don't know where it's coming from, you can't request as well).
Note: DIP and DI aren't the same thing. DIP means Dependency Inversion Principle, while DI means Dependency Injection, which is a way of incorporating DIP.
What's Service Locator (SL)?
Just like DI / Dependency Injection, Service Locator is also a pattern/principle that provides a way to incorporate IoC/DIP, just that SL isn't as strict as DI. It also says the dependency should be provided to the dependant, but in contrary to DI, here the dependant class can request for the dependency in various forms. This can give a big benefit - for example, the dependant class can request for the dependency only when it needs to use it.
Is Constructor Injection a type of Service Locator (SL)?
I believe yes since, with Constructor Injection, the class explicitly asks for the dependency to be passed down to it while creating the instance, which contradicts the DI principle. DI says the dependant should never ask for the dependency in any form or manner.
So is Constructor Injection a bad practice?
Quite the contrary, you should rely on Constructor Injection whenever possible. It makes it more readable and keeps it free from framework interference, for example, if you change the DI framework for your project tomorrow, you won't need to make changes to the dependant class, as its dependencies are passed down the constructor, it'd be same for any DI/SL framework you use.
Moreover, Constructor Injections make it easy for you to test. In my opinion, while writing Unit tests, it's easy to provide the dependencies with Constructor Injection, than with any other methods.
Also, note here, Constructor Injection is not DI per se, but it is definitely an implementation of IoC.
What is Dagger?
Most of Android Developers confuse DI with Dagger. If you casually ask an Android developer, "Hey, do you use DI", more than often you'll get answers like "(no we don't / yes we) use dagger". That sums up how much popular Dagger became in the Android community lately, that. people mixup DI with Dagger. So what is Dagger? Dagger is a framework that helps you implement DI. the difference between DI and Dagger is the same as between DI and Dagger is similar to that of Car and Rolls Royce Phantom.
What Dagger does is that it lets you annotate dependencies of your class with
@Inject annotation (from JSR330), so it can proactively provide those dependencies for you to the dependant class.
Now, it's worth noting here that Dagger or any other DI/SL framework for that matter doesn't know how to create instances of those dependencies, you'll have to tell that to the framework, for Dagger, we generally do that in a module file, or sometimes even in the dependency class with
So what happens when you annotate a dependency with
@Inject but Dagger doesn't know how to create that instance? The answer is simple, your build will fail. To understand why we need to understand how dagger works.
How Dagger works?
We can't explain everything in Dagger in a single post, but here I'd try to give a brief idea of how Dagger code generation works.
Dagger uses annotation processing to generate necessary code, that'll inject the dependency in your dependant class. To explain it further, let us say you have an
Fragment, and you are trying to inject your
RecyclerView.Adapter to that Activity/Fragment, so you annotated
adapter variable in your Activity/Fragment with
@Inject, so when you build the code, annotation processing will take place, and Dagger will generate a file named like
ActivityName_MembersInjector, let us consider the Activity/Fragment is named as
HomeScreen, the injector class name would be
HomeScreen_MembersInjector. Now if you navigate to that generated file, you'll see that it has a dependency on a
Provider<T> for the
adapter, so if we consider your adapter's name is
HomeScreen_MembersInjector would have a dependency on
Now the question is how Dagger will get the provider instance to pass it on to the injector? The answer is simple, for every
@Provides annotation you write, Dagger generates a Provider class for that dependency.
So if Dagger can't find a Provider class for any dependency while generating the code, it'll fail the build.
What's Koin then and how it works?
Koin is another library enabling IoC, it relies on SL pattern rather than DI. Koin utilizes Kotlin's delegation system for providing the dependencies, rather than generating code do to that. With Koin you'll write something like
val adapter: HomeAdapter by inject(), basically you'll use a function named
inject(), which in turn uses Koilin's
lazy delegation. Kotlin's
lazy delegation allows you to instantiate a
val property (final variable) when it's first used, so if you declare a variable, and use
lazy delegation to initialize it, and never use the variable, at runtime, the variable will never be initialized.
So, what happens with Koin's
inject() delegation, is that the class asks for the dependency when it needs at the runtime and then, in turn, Koin searches if you mentioned how to create that dependency in a Koin
module, provides the dependency if you did, or throws a
RuntimeException if you didn't (there's a solution for that though).
Is DI possible Manually?
Absolutely, it requires a lot of effort though to get proper DI manually. What does Dagger do? It generates the code that does the DI, so obviously DI is possible manually, however, think of the volume of code Dagger generates, I'm not sure you'd like to write all that code yourself for every class and all of its dependencies.
What should I do then DI or SL and Dagger or Koin?
The correct answer depends on your team and project size and requirements. Both DI and SL have their fair share of advantages and disadvantages. However, there's one type of SL, that you should always prefer to use whenever possible, that is Constructor Injection (strictly my opinion).
You can use Constructor Injection with both Dagger and/or Koin, so irrespective of the framework you use, it enables you to use IoC / DIP easily. In other words, Constructor Injection helps you achieve KISS.
For Dagger vs Koin, again the choice depends on multiple factors including your project size and requirements, but here are a few pointers that can both be in advantage or disadvantage based on your project/requirement.
- Koin resolves the dependencies at runtime whereas Dagger resolves them at compile time. Now, if you look at it, doing it at runtime might be in your favor, especially if you want your dependencies to be loaded lazily, however, as I pointed out before this can sometimes lead to unwanted surprises and even crashes. If you use Koin or any other SL, you need to have tests in place to make sure there are no surprises. Thankfully Koin provides a gradle task along with a test mechanism to do just that.
- Dagger/Hilt provides a way to automatically bind your Dynamic Feature modules, however with Koin, you need to manually bind them as the modules get downloaded.
- Dagger also has the option of providing dependencies lazily, with it's
Lazywrapper, but with
Lazyas well, Dagger automatically resolves the dependencies at compile time to generate providers, so you're safe. Many people believe that
Lazyis a way to do SL with Dagger.
- One common argument we get against Dagger is "The ugly
public lateinit var", this can be easily avoidable with Constructor Injection for the most part, only for the Framework classes, you'll need this. Now, it's your decision whether you want to avoid Dagger for this or you'd consider it as a tradeoff.
To conclude, we can say the choice between DI and SL or Dagger and Koin is much like a choice between Statically Typed Languages (like Kotlin) and Dynamic Languages (like Python, etc.) none of these are bad, and both have their fair share of advantages or disadvantages, however, either one is better suited in a given scenario, while the other one might be better suited in some other scenario.
Disclaimer: Most of this post is based on my understanding, and my opinions. Also, it's worth noting here, that some good people believe Constructor Injection is DI, not SL, I wrote here what I believe to be appropriate.