Dagger/Hilt vs Koin for Jetpack Compose Apps

Ahh, here we go again. The eternal struggle between Dagger/Hilt and Koin. Let's see which one is better for Jetpack Compose apps!

Dagger/Hilt vs Koin for Jetpack Compose Apps
Photo by Mateusz Wacławek / Unsplash

Intro

Ahh, here we go again. The eternal struggle between Dagger/Hilt and Koin. I expect a lot of arguments again in the comments about which one is better, but don’t worry guys, this article was created only to show you the main differences between them. Both of them are great, so you have to choose which one you want to use on your own, BUT I hope I’ll make this choice a little easier for you by showing you their advantages and disadvantages for Jetpack Compose apps!

For the purposes of this article I’m gonna assume that you already know what Dependency Injection is and what are the main differences between Dagger/Hilt and Koin, for example:

  • Code generation VS no code generation
  • Impact on build time VS impact on runtime and injection time
  • And obviously Hilt is recommended and maintained by Google, while Koin is not. Of course, Google does not say that Koin is bad and to use what you think fits better to your project.

Here’s Manuel Vivo’s opinion about Koin, for example:

Google recommends Hilt for Android apps but that’s just a recommendation! Use what you think it’s good for you.
Hi! I think Koin is a great library as well. Use what you think fits better to your project.
There are pros and cons to both libraries:

If none of this sounds familiar then you should probably stop reading for a moment and first figure out what Dependency Injection, Dagger, Hilt and Koin are before continuing.


Let’s roll

Okay, so now let’s compare Dagger/Hilt and Koin.

To best present the PROS and CONS of each of these tools, I will consider two cases where you can use Jetpack Compose to write Android Apps:

  • The first case  —  when your app is written in pure Jetpack Compose, that is, without using Fragments, so you’re probably using navigation-compose library.
  • And the second case  —  when you use Fragments and ComposeView (Interoperability), which in my opinion is the best choice (at least for now), mostly because of the bad Navigation-Compose API and lack of type safety. This is quite a big and controversial topic, but hey, this is what I like the most 😎

Be sure to check my article about this: https://patrykkosieradzki.medium.com/why-using-navigation-compose-in-your-jetpack-compose-app-is-a-bad-idea-2b16e8751d89

Also, if you’re a Polish speaker, you can listen to my “Android Talks” Podcast episode about it:


PROS of using Hilt in your app

So what are some of the good things about Hilt?

Jetpack and Support

First of all, Hilt is a part of Jetpack and currently Google recommends that you use it in your Android Apps. Of course, that’s not a big deal, but I must say it’s really nice that Google created it, as some of you may know, using Dagger was not always as nice and enjoyable as we would like it to be  —  especially for programmers who were just starting to learn Dependency Injection in Android.

Easier to implement than Dagger

Second thing, like I’ve just mentioned, if you were using Dagger before and you liked it, you’re gonna LOVE using Hilt!

Compile time errors

Dagger and Hilt are compile-time dependency injection frameworks. It means that if we accidentally forget to provide some dependency or we mess something up the build is gonna fail and our app won’t run at all.

Koin will behave differently in this situation. As you know Koin does not generate any code, so if we mess something up with DI the project will build anyway, but it will crash at start or later on some specific screen.

The developer would have to check whether the dependencies in this specific part of the app are working well and the app does not crash. Dagger/Hilt is much safer to use in that case.

Testing

The next big thing is TESTING. For sure, writing Unit, UI, E2E, etc. tests was already possible with Dagger, BUT what we get from Hilt now is much, much more! Forget about complicated tests with Dagger. Now you have HiltAndroidRule to manage the component’s state and to inject different dependencies to tests easier.

@HiltAndroidTest
class SettingsActivityTest {

    @get:Rule  
    var hiltRule = HiltAndroidRule(this)  
    
    // UI tests here.
}

You can easily replace and mock dependencies or even the whole modules on the fly. Also, you can launch your Fragments (if you’re using them) in a special test container launchFragmentInHiltContainer. If you’re not using Fragments or you want to test only the Composable, you can easily do that with ComposeTestRule, where you can pass your ViewModel and other dependencies directly to the Composable Function (example on my Github). It is also possible to automatically mock every ViewModel in your UI tests, so the test setup is only a few lines of code 😉.

Process death

Securing your app against process death is also a lot easier with Hilt. You can simply inject SavedStateHandle to the ViewModel (which is just a fancy StateHandle map) to store and restore data that needs to be saved in case of process death.

CONS of using Hilt

Is Hilt flawless? Let’s find out 🤔

Slower build time

As most of you probably know Hilt generates some files during build time, which means that the bigger the app and the more modules, components and dependencies you have, the longer your build time is gonna get 🐢.

Sometimes you have to write Dagger code

While using Hilt is much easier than it was with Dagger, sometimes you will still need use Dagger code in your app. If you want to create separate android modules per features in your app then you’ll have to write some of your code in the old fashioned way with Dagger. First, you have to create a custom EntryPoint module, combine it inside the component’s builder and then programatically inject it into the Activity or Fragment that hosts the feature. You can read more about it here.

You can’t inject anything other than ViewModel into Composables (at least for now)

One of the major flaws for me is the fact that if you’re writing a Compose only app without Fragments you can’t inject dependencies into the Composables, other than ViewModels.

Probably a lot of you are wondering now, why would you even need something like that at all?

Well, first of all this is certainly a loss of functionality, because before Compose we were able to inject dependencies into Fragments, Activities, etc. and now as Fragments were “replaced” by Composables you’d expect that this feature should be still there, right?

So can you give me some examples when it would be useful?

Sure. Here are a few examples:

  • To delegate UI logic/work to other classes from Composables, so your code is more reusable and concise.
  • To render UI differently, based on data you get from specific dependencies like AppConfig or something else that doesn’t really make sense to put in the ViewModel’s logic (because there’s no logic). Example: I want to display additional text in the Composable if I’m currently in DEBUG mode. I have the isDebug: Boolean value in AppConfig singleton.
  • Another example: Let’s say you need to have multiple Coil ImageLoaders and you want to use them in some of your Composables. You can’t inject them directly into the Composable, so you’d probably have to pass them from the Activity to the NavGraph and then either pass it through Composable params or use CompositionLocalProvider.

Can this problem be solved easily? Yes.

As I told you before, I prefer using Compose with Fragments and this is another reason why I still use them. You can just inject dependencies inside Fragments and then pass them along to Composables.

And what if I want to pass the dependency really deep into the Composable tree?

Well, first think twice if you really need to pass it. If the answer is still yes then you can either pass the variable through each Composable down the tree or you can think about using CompositionLocalProvider. This requires you to write additional code, but it’s still an option.


And how does this relate to Koin?

Let’s star with PROs.

Way easier to use than Dagger and Hilt

First of all, Koin is definitely much simpler to use and to learn than Dagger or Hilt. It can be a good choice for novice programmers that want to learn Dependency Injection.

You can inject dependencies into Composables

Unlike Dagger or Hilt, Koin allows us to inject dependencies into Composables.

For example:

@Composable
fun SomeComposable(myService: MyService = get()) {
   // ...
}

This solves the problem I’ve mentioned before and is really nice to have when we don’t use Fragments.

More informative error logs

If you used Dagger or Hilt before (especially Dagger) you may have noticed that they don’t give much info in the logs about errors that occur and you often have to guess and figure out what is really wrong.

For example, in some cases Hilt would just tell you

[Hilt]

and that’s all.

Of course over time these problems were fixed and you’ll see more info now, but still Koin wins this fight and has more informative logs when errors happen.

No code generation

Koin won’t generate any code at all. This means that your build times will be quicker 🏃

CONS

Much more DI code, especially get(), get(), get()… get()

As I mentioned before, Koin is much easier to implement, but it comes with a price  —  much more code. Every singleton, factory, viewModel, etc. you want to inject you have to add to your modules first.

For example:

val appModule = module {
   single { DogRepository(get()) } 
   
   factory { GetDogUseCase(get()) }
   
   viewModel {
      DogDetailsViewModel(get())
   }
}

So if you have a lot of arguments in your dependencies your modules could end up like this:

val appModule = module {
   single { 
      DogRepository(get(), get(), get(), get(), get())
   } 
   
   factory { 
      GetDogUseCase(
         repo = get()
         cacheRepo = get(),
         service = get(),
         somethingElse = get()
      ) 
   }
   
   viewModel {
      DogDetailsViewModel(
         imagine = get(),
         a = get(),
         lot = get(),
         of = get(),
         dependencies = get(),
         here = get()
      )
   }
}

And this is only one ViewModel and one Repo / UseCase. Imagine how bad it will look in a bigger app…

Issues with SavedStateHandle when not using Fragments

Currently it’s not possible to inject SavedStateHandle to your ViewModels if you don’t use Fragments and inject your viewmodels straight to the Composables. If you try to do that you’ll get an error. This should be fixed soon, but it is something you have to consider if you want to preserve screen state in case of process death.

Impact on runtime performance

As I mentioned before Dagger/Hilt has a significant impact on build time due to code generation. On the other hand Koin also affects time, but not build, but runtime. Koin has slightly worse runtime performance, because it resolves dependencies at runtime.

Source: https://github.com/Sloy/android-dependency-injection-performance

You can find all of the results here:

GitHub - Sloy/android-dependency-injection-performance: [NOT MAINTAINED] Measure the performance of…
This project aims to measure the performance of several Dependency Injection frameworks (or Service Locators) in…

Summary

So which DI should you choose to use? You have to decide yourself. I must admit I’m not a huge fan of Dagger (sorry Dagger lovers), but I would still recommend learning it.

At the end of the day it’s not about which one is better but which one allows you to write clean code that is easy to test and maintain. I used all of them (Dagger, Hilt, Koin) in a few projects before and I think all of them (especially Hilt and Koin) match this criteria.