Blog Infos
Author
Published
Topics
Author
Published
Topics
Posted by: Remco Mokveld

The hilt of a dagger, sword or lightsaber is used to be able to hold the weapon without cutting yourself in the fingers. Hilt is the latest attempt from Google to attempt to prevent you from cutting yourself when using Dagger. Shortly before the alpha release I started playing with it a bit. Being a big dagger-android fan I must say I was skeptical at first, but diving into it I found it to be pretty amazing.

Based on the active development that is happening on GitHub, this article will attempt to explain how Hilt works and how it is different from the dagger-android artifact that it is supposed to replace.

Before I continue, a quick disclaimer. The views in this article are based on what’s in the Dagger codebase before Hilt officially reached alpha. With only documentation in the code to rely on, interpretations might also be wrong. This article assumes previous knowledge of Dagger and dagger-android and does not aim to be a getting started guide. I’ve been in contact with the Hilt team and once Hilt will be released it will include multiple resources for getting started and migrating from Dagger and dagger-android.

With that out of the way, let’s take a look at what Hilt is and what it does. On a high level, it consists of three parts.

  • An AAR artifact which contains a couple of new annotations, and some component and scope definitions. That’s right! That means that, with Hilt, you won’t need to define components yourself anymore. All components and scopes are already defined for you by the library.
  • An annotation processor which saves you from writing tons of boilerplate code. You won’t need to define a component ever again.
  • A Gradle plugin which applies a byte-code transformation to make sure that your source code will use generated classes without you referencing them in source code. Although the plugin is technically not required to be used, it does make everything look a lot prettier.

To set up Hilt, all you need to do is define a dependency on the AAR, add the annotation processor and apply the Gradle plugin. Since it has not been published yet, I had to use a local snapshot, but once released the Gradle logic will look something like this:

// in your root build.gradle
buildscript 

    repositories {
        jcenter()
    }
    dependencies {
        ....        classpath "com.google.dagger:hilt-android-gradle-plugin:2.X"
    }
}
// in modules where you use hilt
apply plugin: 'dagger.hilt.android.plugin'dependencies {
  implementation 'com.google.dagger:hilt-android:2.X'
  kapt 'com.google.dagger:hilt-android-compiler:2.X'}

But, before we continue with implementation, let’s take a step back.

What does Hilt do?

Hilt builds on top of the functionality that Dagger already provides and makes it easier to use Dagger in an Android project. It is an opinionated library so chances are that you might not be able to just plug it into your project (for me that didn’t work).

Being opinionated might be one of the best things about Hilt. Yes, it is a bit more limiting than handling the Dagger beast by itself, and yes, it might mean that it will take a bit more effort to migrate to it. However, there were so many ways in which you could do Dagger before, a lot of them contained tons of unnecessary boilerplate code.

The choices made by the Hilt team might seem limiting to Dagger experts, but in the end it brings uniformity and forces you to use it in a way that has barely any boilerplate code which is a big plus when a large team is working on a project together, or when new members join a project.

Defining bindings

Something that hasn’t changed a lot is how you define bindings. You’ll still have constructor injection, and you’ll still define modules annotated with @Module. Those modules will still have @Binds and @Provides methods.

The one thing that did change is that you will now need to add an additional annotation. Every module in Hilt needs to be annotated with an @InstallIn annotation. This annotation requires one argument which indicates what component the module’s bindings should be used in. @InstallIn(ApplicationComponent::class) on a class called AppModule will have the same behavior as when you would previously do @Component(modules = AppModule::class).

This is a really cool change in the way dependencies are provided because a module definition like this is much clearer on its own.

The reason that this is so much nicer is that, by just looking at the module class, you know exactly where it will be used, and what instances will be available for defining @Provides and @Binds methods. Availability of instances is very dependent on the component that the module is included in. In dagger-android a module could be included anywhere, in some cases the bindings might be valid, in other cases not. Or, in other words, the bindings and the component that they are installed in are logically very tightly coupled. Having tightly coupled concepts in the same file is a huge win.

Defining entry points

The next thing you will need to do is define EntryPoints, first you’ll need to add an @HiltAndroidApp annotation to your application class. This will replace the DaggerApplication class of dagger-android. Annotating your application class with this is all you need to get member injection in the application started. You won’t need to build any component, and you won’t need to call an inject method manually.

You might be asking yourself, how does this work? Well, when you annotate your MyApplication class with @HiltAndroidApp the annotation, hilt will generate a class named Hilt_MyApplication which extends whatever class your application originally extends. So if you had class MyApplication : BaseApplication() the generated class will be class Hilt_MyApplication : BaseApplication(). The generated Hilt_MyApplication will then create the dagger component in its onCreate.

The byte-code transformation that is installed using the Gradle plugin will modify the MyApplication.class file that was generated by javac to change the super class to be Hilt_MyApplication. So in code you have MyApplication extends BaseApplication but at runtime it will actually be MyApplication extends Hilt_MyApplication extends BaseApplication. This is a super clever trick that the Dagger team came up with to significantly reduce the boilerplate code required for Dagger.

Because the annotation processor will read your original super class, and make sure that the generated class has the same super class, this will give you the freedom to use any base class you would want to use for your application, activities or fragments. The generated intermediate class will also do the injection before calling the super.onCreate (or super.onAttachin the Fragment case), so if you have a super class that also has injected dependencies those will be injected before it’s onCreate is invoked.

If you want to make Activities, Fragments, Services, BroadcastReceivers or Views (yes the last one is very cool) also compatible with hilt, all it takes is to annotate them with @AndroidEntryPoint and the same will happen where a super class will be generated for it. To provide bindings for these you can install modules into the following components, and all of these components come with a corresponding scope:

  • ActivityRetainedComponent for providing dependencies that survive orientation changes and basically have the same lifetime as a ViewModelobtained from the Activity. This component will have the ApplicationComponent as parent, so you’ll be able to depend on all bindings defined in the ApplicationComponent. When providers are annotated with the @ActivityRetainedScoped that means that you will get the same instance, even across orientation changes. I think this one will be super helpful.
  • ActivityComponent for providing dependencies that require an instance of Activity. This component will have the ActivityRetainedComponent as parent, so you’ll be able to depend on anything that is available for injection in the ActivityRetainedComponent(which includes everything in the ApplicationComponent). When scoped with @ActivityScoped you will get the same instance as long as that Activity exists.
  • FragmentComponent for providing dependencies that require an instance of the Fragment. This component will have the ActivityComponent as parent, so you’ll be able to depend on all bindings defined in the ActivityComponentActivityRetainedComponent or ApplicationComponent. When scoped with @FragmentScoped you will get the same instance as long as the Fragment exists.
  • ViewComponent for providing dependencies that require an instance of View. This component will have the ActivityComponent as parent, so you’ll also be able to depend on all bindings defined in the ActivityComponentActivityRetainedComponent or ApplicationComponent. When annotated with @ViewScopeddependencies will be the same instance in the lifetime of the View.
  • ViewWithFragmentComponent for providing dependencies that require an instance of the view, and an instance of the Fragment that the view is attached to. This subcomponent will have the FragmentComponentas parent, so you’ll be able to depend on all bindings defined in the FragmentComponentActivityComponentActivityRetainedComponent or ApplicationComponent. When annotated with @ViewScopeddependencies will be the same instance in the lifetime of the View.
  • ServiceComponent for providing dependencies that require an instance of Service. This component will have the ApplicationComponent as parent, so you’ll be able to depend on all bindings defined in the ApplicationComponent. When annotated with @ServiceScopeddependencies will be the same instance in the lifetime of the service.

For some reason there is no component definition for BroadcastReceivers, they are supported in Hilt, but you’d only be able to inject dependencies that are provided in the ApplicationComponent.

Job Offers

Job Offers


    Senior Android Engineer – Big Release Team

    Zalando SE
    Berlin
    • Full Time
    apply now

    Developer (m/w/d) Backend/ Mobile

    Payback GmbH
    Cologne, Germany
    • Full Time
    apply now

    Lead Android Engineer

    ASOS
    London
    • Full Time
    apply now
Load more listings

OUR VIDEO RECOMMENDATION

Jobs

What changed from dagger-android?

Besides being much simpler to set up, there are a couple of things that have changed from dagger-android.

There is only one FragmentComponent definition

In dagger-android you could have fragment bindings, specific to certain fragment implementations. If you included a module in a @ContributesAndroidInjector for an AccountSettingsFragment, in that module you’d be able to define providers that take the AccountSettingsFragment instance as one of the parameters. In Hilt you’ll always install modules into a generic FragmentComponent so you’d only be able to inject the Fragment instance. Note that the generated code will still contain different implementation of the FragmentComponent for each Fragment that is annotated with @AndroidEntryPoint. That generated subcomponent will only contain the providers for the dependencies that are actually required by that Fragment.

A side-effect of this is, that you will probably need to start working a lot more with qualifiers or @Named dependencies. For example, right now with dagger-android you might be binding a different LifecycleObserver for two different fragments. In the one fragment module you have:

@Binds 
abstract LifecycleObserver observer(SettingsObserver observer)

and in another fragment module you can have:

 

@Binds
abstract LifecycleObserver observer(MenuObserver observer);

because these modules would be included in different subcomponents this would work fine. With Hilt, these would both be installed in the FragmentComponent so you would end up with a duplicate bindings error. In cases like this, the easiest solution would be to use qualifiers to bind and inject specific instances of LifecycleObserver.

Nested Fragment subcomponents

In dagger-android, you’d be able to make a nested graph of subcomponents for nested fragment. When you have a childFragment and a parentFragment, the component of the childFragment would be a subcomponent of the parentFragment. That would result in being able to inject anything that is provided from the parentFragment, into the childFragment.

This functionality is not supported in Hilt. No matter how deeply nested your Fragment is, the component will always be a direct subcomponent of the ActivityComponent. This is one of the places where the limitations that Hilt imposes is a good thing, because I’ve worked with nested subcomponents before, and you can do pretty cool tricks with them, but in the end they are often very confusing.

Conclusion

There is still tons more to explore. Like the testing support (which looks to be a total game changer) and support for Jetpack components like ViewModel and WorkManager. Overall I am very impressed by what Hilt does, and how it is implemented. I definitely think it will be an amazing handle for Dagger and will save a lot of boilerplate. I’m very excited to start using it and can’t wait for this to reach its first stable release. .

P.S. Only after writing this I realised that the cutting yourself in the fingers reference in in intro is not an English saying, but it is the literal translation of the Dutch version of shooting yourself in the foot.

Photo by ARTUR KERKHOFF on Unsplash

Thanks to Aaron Rietschlin, Manuel Vivo, and Matt Rein.

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
Jetpack Compose recently got released to production and its release was accompanied with great…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu