Dependency injection is a widely used technique that allows programmers to provide any class with its dependencies, instead of having the classes obtain them themselves. This technique is also considered well suited for the Android development ecosystem. As Android’s official documentation suggests, dependency injection allows programmers to lay the groundwork for good app architecture by allowing for multiple advantages like the reusability of code, ease of refactoring, as well as ease of testing.
Because of its many advantages, as well as the decoupling nature of this pattern, dependency injection is almost a must for every modern Android project. And with so many options for libraries and tools to achieve dependency injection, or DI, it can be difficult to decide what framework to use.
In this article, we will explore the two most popular DI libraries for Modern Android Development (MAD): Dagger’s Hilt, and newcomer, Koin.
To jump ahead:
The Hilt framework is a layer on top of the Dagger DI library. More specifically, Hilt is built on top of the Dagger 2 library, which is currently maintained by Google.
Other than simplifying the Dagger implementation into Android apps, some of the other goals that Hilt has are standardizing sets of components and scopes to make the Dagger setup easier, and providing an easy way to provision binding for different build types, as explained by its documentation.
In other words, using Hilt inherently means using Dagger 2, and they can both simultaneously live in the same project. Let’s take a look at Dagger before we dive into Hilt.
In 2012, Square published the Dagger library for fast dependency injection. Four years later, Google took up the project and introduced the new and improved Dagger 2, the version of the library that Hilt is built on top of.
This open source library is written in Java, and generates plain Java source code at compile time to achieve its injection analysis.
Dagger/Hilt uses a series of annotations to indicate what kind of code to generate for its dependency injection. The main difference between Dagger and Hilt is that the Hilt framework automatically generates a lot of the Dagger setup code that is typically needed by Android projects, which saves developers from encountering too much boilerplate code.
Here’s how you would initialize the Hilt library in your Android application using the Application
object:
@HiltAndroidApp class App : Application() { // Don't forget to declare your Application object in the Manifest! ... }
Once your Hilt application has been initialized, we need to tell Hilt which classes can be injected as dependencies.
To do this, we’ll need to add the @Inject
annotation into the classes’ constructors in order for Dagger to declare them for injection:
class InjectedClass @Inject constructor( private val param: InjectedParam ) { ... } class InjectedParam @Inject constructor() { ... }
Note that all parameters in a class annotated for injection also need to be annotated.
After annotating the classes that are intended to be injected, you need to first start annotating entry points, or dependency containers that are lifecycle-aware, which allow for dependencies to be injected into them.
It is only then that we can use the @Inject
annotation again to bring over the dependencies as needed:
@AndroidEntryPoint class MainActivity: FragmentActivity() { @Inject lateinit var injectedClass: InjectedClass override fun onCreate() { ... } }
Hilt can make an entry point of many Android classes, including Application
, BroadcastReceiver
, View
, Service
, and Activity
, but only if it extends FragmentActivity
. Fragment
can also be used as an entry point, but according to Android codelab, it has to extend Jetpack’s Fragment
class and not the old Android one.
The Dagger Hilt framework is a compile-time dependency. This means that all the code that it generates happens at compile time, as well as all the error checking, which would trigger the build to fail compilation. This makes it very easy to determine if the current DI strategy is faulty or not.
The biggest advantage of Hilt is the fact that Google integrated it into their development ecosystem.
Hilt is Android’s recommended way of achieving dependency injection, and it is now packaged alongside Android Jetpack.
According to Android’s official documentation, Hilt defines a standard way to achieve DI in your application by providing containers for every Android class in your project and managing their lifecycles automatically.
Koin is an open source library by Kotzilla that almost serves as the antithesis of Dagger/Hilt. Created in 2017, Koin is fully written in Kotlin, and it provides a different way of achieving dependency injection.
The way Koin achieves dependency injection is so different from traditional dependency injection that some claim it is not dependency injection at all.
Koin uses a pattern called Service Locator, which, according to Google, works by providing a registry where classes can obtain their dependencies instead of directly constructing them. Meanwhile, traditional dependency injection has another class providing the dependencies for you.
For more information on this debate, check out Elye’s deep dive on the topic. In this article, we’ll stick with dependency injection when talking about the Koin library and its usage.
Instead of guiding itself through annotations, Koin uses modules where we declare all the classes, or factories, that will be injected as dependencies into other classes.
Here’s how you initialize the Koin library for your Android application using the Application
object:
class App : Application() { // Don't forget to declare your Application object in the Manifest! override fun onCreate() { super.onCreate() // Start Koin startKoin { // Feed in Context androidContext(this) ... } } ... }
Once Koin is initialized, adding and injecting dependencies is straightforward.
All dependencies need to live in a module
object, which you then feed directly to the Koin instance. We can do this in the following way:
class InjectedParam() class InjectedClass(val subclass: InjectedParam) val appModule = module { single { InjectedParam() } single { InjectedClass(get()) } } class App : Application() { override fun onCreate() { super.onCreate() // Start Koin startKoin { // Feed in Context androidContext(this) // Load dependencies module modules(appModule) } } ... }
Once your dependencies are in a module, you can inject directly into your source code by using the inject
delegate function:
class MainActivity : Activity() { val injectedClass: InjectedClass by inject() override fun onCreate() { ... } }
Differently to Dagger/Hilt, Koin is a runtime dependency. This means that not only does it not generate any code in compile time, but it will not complain about missing dependencies while you’re building your project.
Instead, an exception will be thrown if your app is missing dependencies, likely followed by a crash.
The biggest advantage to Koin is that it is fully written in Kotlin. With MAD moving towards a more Kotlinized ecosystem, there are many advantages to using Kotlin libraries in Kotlin-first projects.
Additionally, and because it doesn’t live as a layer of another library, Koin is very lightweight, and doesn’t add additional compilation time to your project.
Now that we’ve examined both of these libraries’ backgrounds and specifications, let’s revisit some of the specifics of their implementations, and compare them to get a better sense of their differences.
I think the first and most important question to ask when comparing Dagger/Hilt and Koin is: which one is easier to use? We’ve already gone over the simplest of examples for how to initialize and use both of these libraries, so it really depends on personal preference.
If you’re the kind of developer that enjoys using annotations all over your project to better capture intent, then Hilt might be the better option, so that you can use that aspect of development.
Nevertheless, the broader opinion is that Koin is easier to manage than Hilt. Not only does Koin keep the code pretty centralized, rather than having to tag each individual object, but it also uses Kotlin’s latest and greatest to facilitate the access and injection of your dependencies. And if you simply love your annotations either way, Koin has an annotation strategy that can be used as an alternative or in conjunction with their regular usage.
Knowing when your dependency injection strategy is incomplete, or if it fails, is very important. Because both Dagger/Hilt and Koin libraries are processed differently, this task also differs greatly between both options.
While Hilt compile-time dependency allows us to discover errors a lot faster through a compilation error, Koin will build correctly, and only express its runtime exception if the faulty dependency is triggered.
Both libraries have their advantages and disadvantages, as Hilt’s compile-time analysis also makes the build time of the given application slower as a result.
Each library’s dependency type comes with different impacts on performance.
Hilt’s compile-time analysis and class generation greatly impact the compile time of your application, but everything will get sorted earlier in the build lifetime as well, leaving your runtime performance intact.
On the other hand, Koin’s runtime dependency and code analysis sightly impacts the runtime performance of your application, because all dependencies have to be sorted while the application is running instead of doing so a priori.
At this point, you must be asking yourself: So, which one is better? Ultimately, this depends on the details of your project, the size, age, overall experience of the team. I can’t prescribe a solution for you — that’s a decision each development team must make for themselves!
If you’re reading this article in an attempt to decide between these libraries for your project, here are a few things I suggest considering:
And if these pointers aren’t convincing enough, here’s a chart comparing their attributes that you can follow instead:
Hilt | Koin | |
---|---|---|
Programming language | Java | Kotlin |
Requires Kotlin | ❌ | ✅ |
Kotlin Multiplatform Support | ❌ | ✅ |
Open source | Requires agreement | ✅ |
Approx. package size (.zip) | 20 MB | 1 MB |
Dependency type | Build-time | Run-time |
Dependency injection type | Traditional DI | Service Locator |
Generates code | ✅ | ❌ |
Build-time performance impact | ❌ | ✅ |
Runtime performance impact | ✅ | ❌ |
Error handling | Compilation error | Runtime exception |
Native Components compatibility | ViewModel, WorkManager, Navigation, Compose | ViewModel, WorkManager, Compose |
Choosing a dependency injection library shouldn’t be a burden. Remember that choosing a DI library is not an architectural decision, but a part of implementation details. This means that whatever you choose for your project, make sure that it is integrated in a way that makes it easy to replace, in the case that the initial decision ages poorly, or the alternative options become more convenient over time!
LogRocket is an Android monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your Android apps.
LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.
Start proactively monitoring your Android apps — try LogRocket for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowCompare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.