Ivan Garza Ivan is an Android engineer at Mixhalo, a passionate fútbol fan, and an amateur salsa maker.

Kotlin dependency injection: Koin vs. Hilt

7 min read 1994

Kotlin Dependency Injection: Koin Vs. Hilt

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:

Hilt

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.

A brief history of Dagger

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.

How does Dagger work?

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.

Dagger compile-time dependency

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.

Hilt in Modern Android Development (MAD)

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.

What is Koin?

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.

How does Koin work?

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() {
        ...
    }
}

Koin run-time dependency

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.

Koin in Modern Android Development (MAD)

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.


More great articles from LogRocket:


Comparing Dagger and Koin features

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.

Ease of use

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.

Handling errors

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.

Impact on performance

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.

Dagger vs. Koin

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:

  1. If you have a rather young or new project, consider choosing the library that shares the same main language as your project; i.e., Java projects can work with Dagger, while Kotlin projects instead prefer Koin
  2. If you consider yourself to be more of a novice developer and don’t want to be bothered much with DI, Koin is be a good option because it is considered easier to learn and use
  3. If you’re a developer that loves reading documentation, Hilt may be a better solution as the Android Developers website has thorough documentation, use cases, and even an official codelab, for you to follow
  4. While I didn’t go over Dagger 2’s advanced usage in this piece, a project that may require more advanced usage of DI should probably choose Hilt, as to have the entirety of Dagger available when needed
  5. If your project uses, or is intended to use, the Kotlin Multiplatform tools for developing iOS apps that reuse your Kotlin code, Koin would probably be a better solution, as it currently supports the DI pattern as part of Kotlin’s Multiplatform for iOS

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

Conclusion

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: Instantly recreate issues in your Android apps.

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 — .

Ivan Garza Ivan is an Android engineer at Mixhalo, a passionate fútbol fan, and an amateur salsa maker.

Leave a Reply