Dagger Hilt Review and Tutorial

Dagger Hilt is a new library that wraps around Dagger 2 dependency injection framework. It provides higher-level, opinionated API for Dagger 2 which aims to simplify dependency injection in Android projects.

In this article, I’ll demonstrate Hilt’s basics and share my opinion about this new addition to Android toolkit.

Dagger Hilt is in Early Alpha

It’s important to keep in mind that, as of this writing, Dagger Hilt is in alpha. In my opinion, this library shouldn’t be used in professional projects at this stage. Unless, of course, you don’t mind doing a bit of QA for Google.

I write this post out of personal curiosity and as a service to my students and readers. Please don’t interpret it as an endorsement or a call to action.

Dagger Hilt Mode of Operation

Just like vanilla Dagger, Hilt uses annotation processing and code generation to do its magic. In essence, it’s just one more annotation processor in your app which realizes additional conventions. However, it also supports reflection in tests. This reduces the overhead incurred when you run your test suite and allows for runtime adjustment of object graph (e.g. inject test-doubles in tests).

There is also one aspect of Hilt operation that is based on Gradle plugin. It’s not mandatory, but Google recommends using it.

Dagger Hilt Fundamental Assumption

The fundamental assumption in Dagger Hilt is that dependencies are provided from multiple Component classes which are associated with specific Android constructs. These Component classes form this hierarchy:

The association of a specific class of Component with a specific Android construct means that a new instance of a Component will be created for each instance of Android construct. For example, each Activity will have a dedicated instance of ActivityComponent, each Fragment will have FragmentComponent, etc. Naturally, since there is just one instance of Application, there will be just one instance of the associated ApplicationComponent. Therefore, ApplicationComponent is “global”.

It’s important to note that instances of specific Components will originate from the same Component classes. Therefore, all Android constructs of a specific type will have access to the same set of dependencies. For example, if ActivityA needs DependencyX and you provide it from ActivityComponent, any other Activity in the app will also get access to DependencyX (if needed).

The tree-shaped hierarchical structure of Hilt’s Components is required to ensure that dependencies provided in higher-level Components will be available in lower-level Components as well. For example, going back to the example of DependencyX provided from ActivityComponent, FragmentComponent and ViewComponent will “inherit” this dependency from ActivityComponent. Therefore, you’ll automatically get access to DependencyX in your Fragments and Views (if needed).

Comparison to Dagger-Android

Readers who took my Dagger 2 or Android Architecture courses might be a bit confused now: “What’s all the fuss about? Isn’t the hierarchy of Hilt Components just natural?”.

Well, for me and you Hilt’s hierarchy of components might feel natural. I usually end up with ApplicationComponent, ActivityComponent, ControllerComponent and, if needed, ServiceComponent. That’s also the approach I taught in my courses. Hilt’s hierarchy is just an expanded version of this same structure.

However, Hilt is a 180 degree turn on dagger-android‘s approach, which basically advocated for dedicated Component and Module classes for each Activity and Fragment in your app. I always said that this idea was the biggest fundamental flaw in dagger-android which made it very poor choice for DI (like in this notoriously heated argument with Jake). Happy to see Google finally promoting better practices.

I’m sure that the above two paragraphs read a bit self-congratulatory. Well, not every day I get a confirmation that my recommendations spared the devs who followed them long detours into non-optimal architecture and tens, or even hundreds of thousands of man-hours of cumulative effort.

Injecting Into Application

Since ApplicationComponent sits at the root of Hilt’s hierarchy, it’s mandatory. Therefore, if you want to use Hilt, then, after you set up all the required Gradle dependencies, you annotate your custom Application class (which you must have if you want to use Hilt) with @HiltAndroidApp annotation:

@HiltAndroidApp
public class MyApplication {
    …
}

Then, if you want to inject anything into your custom application, you’ll need to provide that dependency.

Hilt reuses Dagger’s Modules, but inverts the direction of (compile-time) dependencies between Components and Modules. In Hilt, instead of specifying which Modules a Component will use, you specify in each Module in which Components it’ll be installed:

@Module
@InstallIn(ApplicationComponent.class)
public class ApplicationModule {

    @Provides
    Logger logger() {
        return new Logger();
    }
    
}

Once you install a Module into ApplicationComponent, you’ll be able to inject dependencies provided from that Module:

@HiltAndroidApp
public class MyApplication {
    @Inject Logger mLogger;
    ...
}

The injection itself implicitly happens during the invocation of superclass’ onCreate() method. Therefore, if you override it, don’t forget to call through to super.onCreate():

@HiltAndroidApp
public class MyApplication {

    @Inject Logger mLogger;
    
    @Override
    public void onCreate() {
        super.onCreate();
	...
    }

    ...
}

Injecting Into Activities

When you need to inject dependencies into Activities, you annotate that Activity with @AndroidEntryPoint annotation:

@AndroidEntryPoint
public class MainActivity extends AppCompatActivity {
    @Inject Logger mLogger;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
    }
    
    ...
}

Just like with Application, don’t forget to call through to super.onCreate().

Where will an instance of Logger come from? Well, remember I wrote that Hilt’s Components are organized as an hierarchy? Since ActivityComponent is a descendant of ApplicationComponent, it “inherits” the ability to inject Logger.

If you want to add Activity-specific dependencies to objects graph, you’ll need to install additional modules into ActivityComponent:

@Module
@InstallIn(ActivityComponent.class)
public class ActivityModule {

    @Provides
    FragmentManager fragmentManager(Activity activity) {
        return ((AppCompatActivity) activity).getSupportFragmentManager();
    }

    @Provides
    DialogsNavigator dialogsNavigator(FragmentManager fragmentManager) {
        return new DialogsNavigator(fragmentManager);
    }

}

Then you’ll be able to inject DialogsNavigator in your Activity:

@AndroidEntryPoint
public class MainActivity extends AppCompatActivity {
    @Inject Logger mLogger;
    @Inject DialogsNavigator mDialogsNavigator;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
    }

    ...
}

You might’ve noticed that ActivityModule uses Activity, but I don’t provide it anywhere myself. Turns out Hilt automatically provides a small set of implicit dependencies in specific components. Activity in ActivityComponent is one of them. You can find the full list of these dependencies here.

Dagger Hilt Scopes

So far you saw how Dagger Hilt reuses Dagger’s Components and Modules. It establishes a strict convention, which is often referred to as being “opinionated”. Similarly, Hilt reuses Scopes.

Let’s say that DialogsNavigator is a stateful object which must be shared among all the clients within the scope of a single Activity. For example, imagine single-Activity app with many Fragments, all of which must use the same instance of DialogsNavigator.

To implement this requirement in Hilt, you need to annotate that dependency provided form ActivityModule with @ActivityScoped annotation:

@Module
@InstallIn(ActivityComponent.class)
public class ActivityModule {

    @Provides
    FragmentManager fragmentManager(Activity activity) {
        return ((AppCompatActivity) activity).getSupportFragmentManager();
    }

    @Provides
    @ActivityScoped
    DialogsNavigator dialogsNavigator(FragmentManager fragmentManager) {
        return new DialogsNavigator(fragmentManager);
    }

}

Then in each individual Fragment shown in that Activity you do:

@AndroidEntryPoint
public class MyFragment extends Fragment {

    @Inject DialogsNavigator mDialogsNavigator;

}

It’s guaranteed now that the Activity and all the Fragments within the scope of that Activity will get a reference to the same instance of DialogsNavigator.

However, if you change to a different Activity, different instance will be created for it and its own Fragments.

Dagger Hilt and ViewModel

As far as I understand the docs, you can annotate ViewModels’ constructors with @ViewModelInject annotation and Hilt will generate the respective factories and stuff behind the scenes. This will allow you to simply @Inject ViewModels into Activities and Fragments. In addition, you can use @Assisted annotation to automatically pass SavedStateHandle into ViewModels’ constructors.

This looks like quite a big quality-of-life improvement for ViewModel users. I don’t use ViewModels myself, so I never experienced their pains and limitations. Therefore, if you try this approach, let us know what you think about it in the comments.

Advanced Dagger Hilt

So far, I covered the basics of Hilt. It should get you started with Hilt and its official documentation, but it wasn’t an exhaustive list of Hilt’s features. Stuff that I didn’t cover: custom components, custom entry points, scope aliases, optional injection, extensions, testing, and, probably, some more.

As far as I can tell, many apps won’t even need to deal with the above “advanced” concepts because the basics seem to provide sane defaults. In addition, the official documentation is surprisingly good, so if you’ll be evaluating Hilt for real production use, just go over the docs first.

Edit: turned out I introduced a critical bug when I refactored IDoCare to Hilt. To fix this bug, I had to use Hilt’s advanced feature called Entry Points. You can read about that in this follow-up article.

Automated Testing with Dagger Hilt

One very interesting aspect of Hilt is its support for automated testing.

Let’s be clear about one thing right away: if you write unit tests for your code, these tests shouldn’t know anything about dependency injection frameworks. Hilt’s testing features should be restricted to integration, end-to-end and UI tests (whether you use real device, emulator, or Robolectric).

Automated testing has always been Dagger’s weak spot because it makes it very hard to replace dependencies with test-doubles in tests. This is an inherent limitation stemming from Dagger being an annotation processor and a code generator. To mitigate this limitation, Hilt makes use of reflection in tests. This is an interesting approach, but, still, looks like you can’t just replace individual dependencies and need to nuke out entire modules.

It’s definitely too early to assess Hilt’s utility in automated testing, but the fact that this aspect got a bit of attention and love is encouraging.

Migration to Dagger Hilt

To get my hands dirty, I migrated my “experiments app”, IDoCare, to Dagger Hilt.

As I wrote above, since the fundamental assumptions of Dagger Hilt are aligned with what I’ve been doing for years, it took less than two hours. Therefore, if you’ve been following my recommendations, your app should be ready for Dagger Hilt and you just need to wait for it to reach maturity.

You can review the differences by comparing repo’s master branch with dagger-hilt branch.

Note: IDoCare is not a “clean code” in my opinion. That’s my first real app and I use it today to test ideas and tools, so don’t copy stuff from there blindly.

Dagger Hilt Best Practices

I know it looks a “bit” odd when someone shares best practices for a library that was released two days ago. Literally. However, as I mentioned above, Hilt basically follows the same structure of DI that I used for years and teach in my courses. Therefore, even though the lib itself is new, the underlying architectural approach is not and I have a lot of experience with it. Therefore, even though there is a real possibility that I can be wrong, I feel that my advice can spare much time to others, so here we go.

First, out of all Hilt’s Components, the “core” components are ApplicationComponent, ActivityComponent and ServiceComponent. In some cases, like when you use nested Fragments in some clever way, you might need FragmentComponent as well, but I’d try to avoid that if possible.

Don’t use ViewComponent and ViewWithFragmentComponent at all, unless you migrate messed-up legacy project to Hilt.

It looks like ActivityRetainedComponent is basically equivalent to our “old friend”, retained Fragment, in disguise. We quit using it long ago and then Google deprecated the concept itself (IIRC), but now it makes a comeback in Hilt. For relatively new developers, ActivityRetainedComponent can seem as a superpower on the first sight, and in some sense it is. However, I expect to see most of the usages of this construct to actively harm the maintainability of Android projects, just like retained Fragments did. Try to avoid it.

In all the above examples, I explicitly provided dependencies inside Modules using @Provides annotation. You don’t have to do that and using @Inject annotated constructors is also a valid implementation choice. However, for scoped dependencies, I recommend using this explicit approach exclusively. There is real maintenance benefit in being able to open a couple of Modules and get a full picture of scoped dependencies in the app.

Somewhere I read a recommendation to use @Reusable annotation. The best thing you can do with it is to forget about its existence.

There is also a section titled “extensions” in Hilt docs which seem to suggest that Google envisions third-party libraries that will depend on Hilt and integrate into apps. Sounds like a very bad idea to me. Don’t make your libs depend on any DI framework.

And, most importantly, remember that Dependency Injection is a complex architectural pattern that has tremendous effect on the long-term maintainability of your project. DI frameworks provide a template and some conventions, but the quality of your DI doesn’t automatically increase if you use a framework. Therefore, learn the fundamentals and keep the big picture in mind.

My Concerns About Dagger Hilt

Google’s PR and many other resources will assure you that Dagger Hilt will make DI so much simpler and eliminate the boilerplate and everything will be great. I will tell you the truth: there are many potential pitfalls and it’s not even evident at this point that Dagger Hilt justifies its existence.

First of all, I don’t understand Google’s obsession with “boilerplate”. It looks like that’s the main criteria by which they evaluate architectural decisions. Dagger Hilt is not an exception and the current docs state “Reduced boilerplate” as Hilt’s very first benefit:

Here is an interesting question: in your estimation, how many lines of code did I eliminate by migrating IDoCare to Dagger Hilt? The answer is that the diff between branches stands at 119 lines of code. However, since I refactored and simplified quite a bit of logic during the migration (e.g. removed entire unneeded ServerSyncComponent, removed unneeded scope annotations, etc.), the actual apples-to-apples diff would probably be around 50 lines of code.

Granted, IDoCare isn’t big project (14 KLOC), but the amount of spared “boilerplate” wouldn’t be much higher even if IDoCare would be ten times in size because most of this code was centralized and reused. This kind of duplication elimination is something that I put special emphasis on in my code. It might look as an overkill on the first sight, but then, at some point, I need to migrate to a different approach and realize that five minutes I invested two years ago spare me hours of work today.

The migration of Google IO (which used dagger-android) to Hilt resulted in removal of ~600 LOC (that’s 44 KLOC application). Not something to be extasic about either, especially given how verbose and needlessly complicated dagger-android is to begin with.

So, I spent three hours reading docs and another two hours migrating the app to Hilt. Even if Hilt won’t have any downsides (very improbable) and I’ll use it in the next 10 projects, I still won’t get positive ROI from it. This time is basically wasted and Hilt for me, as a developer, will probably remain net negative forever. Don’t worry, that’s not the only criteria, so it’s not that bad. I just want you to understand how nonsensical and amateurish “boilerplate” argument is in this context.

All in all, while Google is finally on track to some sane architectural pattern, they can’t get over their “boilerplate” fetish. In this context, googlers are like miners who found gold in their shaft, but can’t notice it due to their obsession with coal.

The second concern I have about Hilt is increased build times.

Dagger is notoriously bad in context of build times, especially if you use Kotlin and Kapt. I know projects that use Kotlin and Dagger where build times are one of the worst productivity killers. Lyft folks, for example, invested into some kind of black magic to use Dagger without Kapt. It’s a real mess.

It seems to me that Dagger Hilt adds additional layer of annotation processing and code generation on top of standard Dagger. If this addition will increase the build times in a noticeable way, integration of Dagger Hilt would be very questionable move. This article is already too long and I don’t have any time left, but I’d recommend benchmarking IDoCare and IOSched to see the impact of Dagger Hilt on build times (but even then remember that these are small apps).

In addition, Dagger Hilt uses Gradle plugin for a bit or additional black magic. As far as I understand, you can get rid of it, but the examples in docs assume that it’s being used. I didn’t look into this aspect much, but it looks like this plugin is used to spare another couple of lines of code. If that’s the case, how many additional “invalidate caches and restart” would you tolerate due to this plugin instead of writing that code yourself?

My last concern about Dagger Hilt is whether it’s too little, too late. I get a sense that developers start being tired of Dagger and Google’s games. In new Kotlin-only projects, Koin seems to become popular and the overall feedback about it is very positive. Even if Hilt won’t have any disadvantages (again, improbable), maybe it’s not the best investment of our time going forward if the ecosystem is already Kotlin first? However, that’s just my concern and only time will tell whether it’s valid.

Conclusion

I started this post with the intention to share a quick opinion on Dagger Hilt. I failed miserably, once again. On the other hand, I do think that the info in this article can help many developers, so I hope you’ll find it useful.

The good news are that Google finally abandoned the idea behind dagger-android which is a big step forward. I’m sure that someone there had to fight a good fight for this to happen, so hats off. The defaults of Dagger Hilt seem reasonable to me. The documentation is much better than it used to be and I understood what this lib does without much back and forth. Migration guides are also very clear and actually helpful (even though I didn’t need them). New APIs are pretty straightforward to use. ViewModel injection also seems to become easier, but it’s largely irrelevant for me as a developer (but not as an instructor, unfortunately).

I can’t say anything definitive about automated testing story yet.

On the negative side we have a real concern about Hilt’s effect on build times. I’d say that’s the main question to Google right now. Docs still obsessed with “boilerplate”, even though it’s the most irrelevant metric in this context. Not sure why Gradle Plugin is required. Also a bit too many Components and scopes (though I understand that they probably wanted to cover all bases).

In one of my previous posts, which was my triumphant eulogy over dagger-android‘s coffin, I explained why I’m skeptical about Google’s effort to “improve Dagger” and promised to extend them my apology if they prove my skepticism wrong. After writing this long article and taking into account all my concerns and criticism, I think that, overall, they did a good job and Hilt has a real chance to become that standard of DI that Android needed so much in the past 11 years. It has a long way to go, and it’s not evident that it’ll reach that point, but I still want to congratulate googlers who worked on Hilt. It’s a fair and decent attempt to standardize Dagger integration into Android projects. Googlers, if you read this, I apologize for not believing in you.

Said all that, for some reason, Google already recommends Dagger Hilt as DI solution for Android, two days after the release of the first alpha version. I find it odd and I won’t use Dagger Hilt in professional project any time soon. Especially given the fact it doesn’t provide me any immediate benefits.

As usual, thanks for reading and leave your comments and questions below.

7 comments on "Dagger Hilt Review and Tutorial"

  1. Thanks for another great article!

    Sorry if I missed something, but I have noticed that you changed the order when setting the FragmentFactory to the FragmentManager of your Activity (https://github.com/techyourchance/idocare-android/commit/a2687cf4791c3e4bea7d30ae706dbae9292ca3cd#diff-7509d402ec31a6d0b882fd508a4a47c5R69), probably because the FragmentFactory is only injected after the call to super.onCreate()? Wouldn’t that break fragment recreation (https://proandroiddev.com/android-fragments-fragmentfactory-ceec3cf7c959)?

    Reply
    • Guilherme, your code review skills are amazing!
      Yes, that was the reason I had to change the order of these calls. To be honest, I didn’t think it through because I just wanted to “make it work”, but now when you bring it up, I see that it’s a valid concern. Not sure if it’s safe, but it’s also not difficult to test. I don’t have time right now, but will try to remember to come back and test this point later.
      On the fundamental level, that’s yet another example how “simplifying” things can cause unexpected side-effects, especially in relation to Android lifecycles. If this is indeed a problem, then I can’t even think of a solution from the top of my head.

      Reply
  2. Just curious, I couldn’t find any big companies adopting koin over dagger. Dagger hilt looks so promising which removes all the boilerplate codes. I am in the dilemma between koin(just started looking) vs dagger(hilt) for my next project.

    Reply
  3. Nice article but you can’t blame googlers for boilerplate obsession. It started with Jake’s butterknife obsession with findViewById() as if that was gonna solve android world problems.

    Things like boilerplate and other terminology was then widely used for Hype driven development (RxJava etc) and to push someone’s agenda. Google is now just following the same to push their libraries, no different.

    Reply
  4. Hi Vasiliy,

    “It looks like ActivityRetainedComponent is basically equivalent to our “old friend”, retained Fragment, in disguise. We quit using it long ago and then Google deprecated the concept itself (IIRC), but now it makes a comeback in Hilt.”

    ViewModel implementation was changed from retained fragment to activity non configuration instance. Can you clarify this?

    Reply
    • Hi,
      I’m not sure what’s the underlying mechanics of ViewModel and it’s not that important. What this paragraph means is that retaining objects using ActivityRetainedComponent is very similar to using retained headless Fragment. This option has been (ab)used when Fragments just came out, but it became evident quickly that it’s not something you need in absolute majority of apps.

      Reply

Leave a Comment