Deep dive into Dagger generated code (part 2)
By: Alexander Sitnikov
Tags: Kotlin, Android, Android App Development, Dagger 2, Dependency Injection

Photo by Lum3n from Pexels

In the previous blog post, we looked at the different types of classes Dagger generates. Converting the generated code from Java to Kotlin with some additional refactoring removes visual noise and helps us understand it better. In this blog post, we continue to dive into how different features are implemented in Dagger — component instantiation, members injection, qualifiers and multi-bindings. Most of those features were introduced after the initial release of Dagger, and were build on top of things discussed in the part 1 of the series.

In order to follow the rest of the article, it is highly recommended to read part 1 of the series.

Component instantiation

In Dagger there are two ways to instantiate the component — Builder and Factory . By default, Dagger generates a Builder . We can make Dagger create a custom implementation of a Builder — for that, we have to create an inner interface and annotate it with @Component.Builder .

As a result, the generated Builder will implement the declared interface. For every setter method, there will be fields generated inside Builder . Also static builder() method will be generated in the component, that will return an instance of that builder.

When using Builder , you can forget to call one of the setter methods, causing exceptions during runtime. To avoid that, Dagger v2.22 added the ability to create @Component.Factory for component instantiation. This Factory is stateless and has a single method, so the program will fail at compile time instead of runtime if you forget to declare some of the component dependencies.

As we can see below, this single method just instantiates the component, without any temporary fields.

@BindsInstance

One of the most useful features of the custom Builder or Factory is the ability to define @BindsInstance . It allows passing external instances to the component without declaring modules or dependencies.

The instance passed into CustomFactory.customCreate() will be stored in the field inside the component, thus making it singleton for this component.

In the generated code you can notice that stringProvider property is defined using InstanceFactory . It is the internal Dagger class that simply returns the same instance for every get() .

To further simplify things, let’s add a new rule.

5. Replace all generated factories and InstanceFactory with a lambda

@Component.Factory and component fields

So we can use @Component.Factory to pass stateful modules, dependencies, and binds instances to a component. Let’s create a factory that accepts all of the different types of parameters.

As we can see below, all of them are stored as fields inside the component. With that in mind, you can treat all the parameters of @Component.Factory as constructor parameters of the component.

Members injection

Sometimes we don’t control the lifecycle of an object — we can’t instantiate it in @Provides method or add @Inject constructor to it. This is when members-injection comes to the rescue:

Dagger generates a members-injector for classes with @Inject applied to a field or a method. MembersInjector is the functional interface (lambda) defined in Dagger, and it is implemented by all generated members-injectors.

public interface MembersInjector<T> {

void injectMembers(T instance);

}

Let’s take a look inside the generated members-injector. For every member of the target class, Dagger generates a static method ( injectRepository() in the example above), that assigns the given instance to a field or calls a setter of a target object. Inside the overridden injectMembers() method, all of these generated methods are called using instances, obtained from Provider s.

Super members injection

There is some special case when class inheritance involved in member’s injection. Let’s take a look at the simple case when one class extends other, and both have members with @Inject .

In that case injectMembers() of generated ChildActivity_MembersInjector calls a static injectString() of parent’s BaseActivity_MembersInjector . So we can come to a conclusion, that if you pass subtype instance to the supertype members-injection method, members of subtype will not be injected.

Now let’s take a look at the generated component. Private methods injectBaseActivity() and injectChildActivity() have the same content as in the generated injectMembers() of the respective class, and also return the injected instance (because you can make component’s members-injection methods return the injected instance).

Again, inlining all the calls, the generated component code becomes easy to grasp.

Methods of component

To summarize, we can declare different types of methods in a component: provision methods and members-injection methods. We can also create methods, that return Provider and MembersInjector directly.

Two main purposes of the component are to create new instances and inject instances into members of already created objects, and they are accomplished by declaring provision and members-injection methods in the component interface. We can also declare methods, that directly return Provider or MembersInjector , to defer the effect of their actions. Let’s take a look at all of those methods in one component.

Because Provider and MembersInjector are functional interfaces, and Kotlin has build-in support for them, we can define some new rules of conversion:

6. Replace Provider<T> and Factory<T> with () -> T

7. Replace MembersInjector<T> with (T) -> Unit

You can notice, that methods of the same purpose are almost identical, but in one case they are executed immediately, and in the other lambda is returned to be executed on demand.

@Binds methods

In the early versions, there was no way to tell Dagger to put implementations where interface was requested — one had to create @Provides method with one parameter of implementation type that returned interface type. That resulted in unnecessary factory generation. So, in Dagger v2.4 @Binds annotation was released.

@Binds methods are declared in modules and just tell Dagger to delegate bindings from superclass/interface to implementation. For example, suppose we have an interface — CharSequence , and some implementation — CharSequenceImpl . We can instruct Dagger to put CharSequenceImpl where CharSequence is requested by declaring an abstract @Binds method in the module.

A thing to note is that @Binds annotated methods are never called by the generated components.

There is one strange limitation in Dagger that @Binds methods are required to have different names, despite the fact that they are never called and no factories generated for them. If you find it inconvenient — upvote this issue.

Qualifiers

@Qualifier is annotation defined in JSR-330. It is used to disambiguate between different instances of the same type. In Dagger, it can be applied to every place where binding is defined: @Provides and @Binds methods of a module, @BindsInstance of a Builder \ Factory , provision method of a component dependency; or where binding is consumed: @Inject constructor parameter, @Provides method parameter, component’s provision method, @Inject to a field or a method.

JSR-330 defines one default qualifier annotation — @Named . Qualifiers can have annotation attributes — and @Named has one of type String . We can also create our own qualifier annotations — we just need to annotate it with @Qualifier .

Let’s take a look at the next example, where different types of qualifiers are applied to different targets.

Note that qualifiers are just instructions for the Dagger compiler, they do not appear in the generated code.

Kotlin properties and qualifiers

You should be extra careful when using Kotlin properties with qualifiers. Consider the following component configuration.

This component won’t compile: Dagger’s annotation processor fails with [Dagger/MissingBinding] java.lang.String cannot be provided without an @Inject constructor or an @Provides-annotated method .

That’s because @Named annotation applied to property, and in Java, there are multiple elements, that correspond to Kotlin property declaration. By default, if both targets are applicable, annotation applies to a property instead of a field, so when compiled for JVM, it gets applied to a special method of the special DefaultImpls inner interface, not to the getter method.

To fix this, we need to do one of two things: either use a method or specify getter as an annotation target.

To avoid issues of this kind, don’t use properties with qualifiers as component provision methods (and as component’s dependencies).

Multibindings

Multibindings is a feature, that allows you to accumulate bindings from one or multiple modules of component into a map ( @IntoMap ) or into a set ( @IntoSet ). When contributing to a map, you also have to define a key for binding — either by using one of the predefined annotations @StringKey , @LongKey , @IntKey or @ClassKey , or by creating custom annotation annotated with @MapKey .

As we can see below, Dagger creates a map and a set using methods from @Provides modules. Keys for the map are statically defined in @StringKey annotations.

End of part 2

In this blog post, we looked at component factory, member-injection, module binds, qualifiers, and multibindings. There are two more big topics to cover: scopes and subcomponents. You can find all of the examples in the GitHub repository. See you next time!