March 14, 2023

Dagger 2

Dagger 2 dependency injection for Java and Android

Dagger implements the dependency injection pattern without the burden of writing boilerplate code. Dagger 2 was the first DI framework to implement the full stack with generated code, and its guiding principle is to generate code that mimics what a developer might have hand-written — keeping dependency injection simple, traceable, and performant.

Declaring the Dependency

Add Dagger to your Gradle build:

dependencies {
    def dagger_version = "2.20"
    ... ...
    ... ...
    implementation "com.google.dagger:dagger:$dagger_version"
    implementation "com.google.dagger:dagger-android:$dagger_version"
    implementation "com.google.dagger:dagger-android-support:$dagger_version"
    // if you use the support libraries
    annotationProcessor "com.google.dagger:dagger-android-processor:$dagger_version"
    annotationProcessor "com.google.dagger:dagger-compiler:$dagger_version"
}

@Inject

Dagger uses the javax.inject.Inject annotation to identify which constructors and fields it should be interested in.

Use @Inject to annotate the constructor that Dagger should use to create instances of a class:

class Test{
    private final Sample sample;

    @Inject
    public Test(Sample sample){
      this.sample=sample;
    }
}

Field Injection

class Test{
    @Inject
    private final Sample sample;

    @Inject
    private final AnotherSample anotherSample;
}

Having @Inject-annotated fields but not a constructor implies that the caller will explicitly create an instance of the class themselves — Dagger will inject the fields but won't construct the object. So it's wise to add a default constructor annotated with @Inject too. Keep in mind that classes without @Inject anywhere cannot be constructed by Dagger at all.

In the normal case above, Dagger creates an instance of a class using the constructor annotated with@Inject and sets all the injectable fields, ready for us to use. But if there's a restriction that prevents us from adding @Inject to a constructor, Dagger offers an alternative: @Provides. This comes up when we need to inject third-party classes, or when a configurable object needs to be configured before it's handed out.

@Provides

The return type determines which dependency a @Provides method satisfies (the method name itself doesn't matter, though the convention is provideClass()):

@Provides
public Gson provideGson(){
    GsonBuilder builder = new GsonBuilder();
    builder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES);
    return builder.create();
}

@Provides methods can have dependencies of their own, but they must belong to a module — a class annotated with @Module:

@Module
public class RestModule {

    @Singleton
    @Provides
    public Retrofit provideRetrofit(Gson gson,OkHttpClient okHttpClient){
      return new Retrofit.Builder()
                .baseUrl(BASE_URL)
                .addConverterFactory(GsonConverterFactory.create(gson))
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .client(okHttpClient)
                .build();
    }

}

Building the Graph

Dagger needs an access point to reach the graph formed by the @Inject- and@Provides-annotated classes. We provide that by applying the @Component annotation to an interface whose methods take no arguments and return the desired type. All the relevant modules are supplied to the @Component, and Dagger generates an implementation of the contract.

@Component

Every type annotated with @Component must contain at least one abstract component method. Component methods can have any name, but their signatures must conform to either aprovision or a members-injection contract.

Provision methods take no parameters and return an injected or provided type, for example:

SomeType getSomeType();
Set<SomeType> getSomeTypes();
@PortNumber int getPortNumber()

Members-injection methods take a single parameter and inject dependencies into each of the@Inject-annotated fields and methods of the passed instance. A members-injection method can be void, or return its single parameter as a convenience for chaining:

void injectSomeType(SomeType someType);
SomeType injectAndReturnSomeType(SomeType someType);

A method with no parameters that returns a MembersInjector is equivalent to a members-injection method. Calling MembersInjector.injectMembers(T) on the returned object performs the same work as a members-injection method, for example:

MembersInjector<SomeType> getSomeTypeMembersInjector();

where MembersInjector<T> injects dependencies into the fields and methods of instances of type T, ignoring the presence or absence of an injectable constructor:

injectMembers(T instance)

Whenever a component creates an instance, it performs this injection automatically — after first performing constructor injection — so if you're able to let the component create all your objects for you, you'll rarely need to call this method yourself.

Scoped Bindings

@Singleton on a @Provides method or an injectable class guarantees a single instance of the value across all clients.

@Reusable can be used to limit how many times an @Inject-constructed class is instantiated, or an @Provides method is called, when we don't need a guarantee of the exact same instance being returned every time. This binding isn't associated with any single component — each component simply caches the instantiated object it creates. Using @Reusable where mutable objects are returned isn't recommended, since callers may expect to be handed the same instance.

Lazy Injections

For any binding T, we can create a Lazy<T> that defers instantiation until the first call to its get() method:

class Test {
  @Inject
  Lazy<Actor> lazyActor;

  public void doStunt() {
      lazyActor.get().doNothing();
  }
}

Qualifiers

If type alone isn't sufficient to distinguish a binding, we can use a qualifier annotation. The following example is fairly self-descriptive:

class Server {

  @Inject
  @Named("vodka")
  Alcohol strongDrink;

  @Inject
  @Named("beer")
  Alcohol lightDrink;

}

The qualified values are then declared like this:

@Provides
@Named("vodka")
static Alcohol provideStringDrink() {
  return new Alcohol(45);
}

@Provides
@Named("beer")
static Alcohol provideLightDrink() {
  return new Alcohol(5);
}