Luciano Serruya Aloisi

Software developer from Trelew, Chubut, Argentina 🇦🇷

Notes on Dagger for Android

These are some notes I wrote back when I was doing some Android development and started learning about Dagger. I'm quite fond of Collected Notes now so I might as well share them!

You can find a demo app implementing Dagger + Architecture Components here


Install

apply plugin: "kotlin-kapt"

dependencies {
    implementation "com.google.dagger:dagger:{version}"
    kapt "com.google.dagger:dagger-compiler:{version}"
}

@Component

An interface annotated with @Component gives the information Dagger needs to generate the dependencies graph at compile-time, manage them and be able to get dependencies from the graph. The parameters of the interface methods define what classes request injection. @Component will make Dagger generate code with all the dependencies required to satify the parameters of the methods it exposes

@Component(...)
interface AppComponent {
    .
    .
    .

    fun inject(activity: MyActivity)
}

@Inject

Adding the annotation @Inject to a class will tell Dagger how to create a dependency. The constructor parameters will be the dependencies of that class.

So, with the @Inject annotation, Dagger knows how to create instances of the class, and what are its dependencies.

In case we are not able to instanciate certain object (for example, Activities and Fragments), we need to specify its dependencies not as constructor dependencies, but as field-level dependencies. To accomplish this, we need to annotate with @Inject the fields that we want Dagger to provide. We then need to obtain the app component and inject the Activities/Fragment dependencies calling the inject method and passing this as an argument in the onCreate method for Activities (before calling super - to avoid issues with fragment restoration), or in the onAttach method for Fragments (it can be done before or after calling super)

class MyViewModel @Inject constructor(...)
class MyActivity : AppCompatActivity() {

    @Inject
    lateinit var myViewModel: MyViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        (application as MyApplication).appComponent.inject(this)
        super.onCreate(savedInstanceState)
    }
}

There are two different ways to interact with the Dagger graph:

  1. Declaring a function that returns Unit and takes a class as a parameter allows field injection in that class (i.e fun inject(activity: MyActivity))
  2. Declaring a function that returns a type allows retrieving types from the graph (e.g fun mySubcomponent(): MySubcomponent.Factory)

@Module

Dagger Modules give us another way to provide instances of a type (mainly user defined interfaces). A Dagger Module is a class annotated with @Module. There we can define how to provide dependencies with the @Provides or @Binds annotations. Modules are a way to encapsulate how to provide objects in a semantic way.

You can include modules in components but you can also include modules inside other modules. Once a module has been added to either a component or another module, it's already in available in the Dagger graph, which means Dagger can provide those objects in that component. Before adding a module, check if that module is part of the Dagger graph already by checking if it's already added to the component or by compiling the project and seeing if Dagger can find the required dependencies for that module.

Good practice dictates that modules should only be declared once in a component.

@Binds

Use @Binds to tell Dagger which implementation of an interface it needs to use when injecting the interface; it must annotate an abstract function (the class needs to be abstract too). The return type of the abstract function is the interface we want to provide an implementation for. The implementation is specified by adding an unique parameter with the interface implementation type (which should be annotated with @Inject so Dagger knows how to create it)

@Module
abstract class MyModule {
    @Binds
    abstract fun provideMyDependency(dependency: MyDependencyImpl): MyDependency
}

@Provides

Apart from @Inject and @Binds annotations, we can use @Provides to tell Dagger how to provide an instance of a class inside a Dagger Module.

The return type of the @Provides function tells Dagger what type is added to the graph. The parameters of that function are the dependencies that Dagger needs to satisfy before providing an instance of that type. @Provides tell Dagger how to create instances of the type that that function returns

@Module
class MyModule {
    @Provides
    fun provideMyDependency(...): MyDependency {
        return MyDependencyImpl(...)
    }
}

@BindsInstance

@BindsInstance tells Dagger that it needs to add some dependency in the graph (created outside of the dependency graph, for example Context) and whenever that instance is required, provide that object. With @BindsInstance, that dependency will now be available in the graph

@Component(...)
interface AppComponent {
    @Component.Factory
    interface Factory {
        fun create(@BindsInstance context: Context): AppComponent
    }
}

@Scope

Dagger's default behaviour is to always provide a new instance of a type when inject dependencies. In case we want share the same instance across several objects (or if an object is very expensive to create, and we do not want to create a new instance every time it's declared as dependency), we can use Scopes. Using Scopes, we get an unique instance of a type in a Component. This is what is also called to scope a type to the Component's lifecycle. Scoping a type to a Component means that the same instance of that type will be used every time the type is provided by a Component.

@Scope
@MustBeDocumented
@Retention(value = AnnotationRetention.RUNTIME)
annotation class MyScope

Add scope annotations in classes when using constructor injection (with @Inject) and add them in @Provides methods when using Dagger Modules.

@Singleton

For scoping types to AppComponent's lifecycle, we can use the @Singleton scope annotation that is the only scope annotation that comes with the javax.inject. If we annotate a Component with @Singleton, all the classes also annotated with it will be scoped to the annotated Component

@Subcomponent

Subcomponents are components that inherit and extend the object graph of a parent component. Therefore all object provided in the parent component will be provided in the subcomponent too. In this way, an object from a subcomponent can depend on an object provided by the parent component.

@Subcomponent
interface MySubcomponent {
    @Subcomponent.Factory
    interface Factory {
        fun create(): MySubcomponent
    }

    fun inject (...)
}

We also must define a subcomponent factory inside our subcomponent so that our main component knows how to create instances of our subcomponent

To include subcomponents to our app's component, we need to define a Module which lists every subcomponent, and then add that Module to our main component.

@Module(subcomponents = [MySubcomponent::class])
class AppSubcomponent
@Component(modules = [AppSubcomponents::class...])
interface AppComponent {
    .
    .
    .
    fun mySubcomponent()
}

Scoping subcomponents

If we annotate the Component and classes with the same scope annotation, that'll make that type to have an unique instance in the Component.

The scope annotation's name should be named depending on the lifetime it has since annotations can be reused by sibling Components

@MyScope
class MyViewModel @Inject constructor(...) { ... }
@MyScope
@Subcomponent
interface MySubcomponent { ... }

Scoping rules

  • When a type is marked with a scope annotation, it can only be used by Components that are annotated with the same scope
  • When a Component is marked with a scope annotation, it can only provide types with that annotation or types that have no annotation
  • A subcomponent cannot use a scope annotation use by one of its parent Components

Subcomponents lifecycle

The lifecycle of a Subcomponent (or a Component) is the same lifecycle as the object that instanciated it. For example, if the Application class creates the AppComponent, then AppComponent's lifecycle will be Application's lifecycle. If an Activity creates a Component, the Component will provides the same instances as long as that Activity is active

@Qualifier

Qualifiers are useful when we want to add different implementations of the same type to the Dagger graph. A qualifier is a custom annotation that will be used to identify a dependency.

@Named

@Named annotation allows us to provide a String to differentiate several objects of the same type

@Module
open class Bag {
    @Provides @Named("Love")
    fun sayLoveDagger2(): Info {
        return Info("Love Dagger 2")
    }
    @Provides @Named("Hello")
    fun sayHelloDagger2(): Info {
        return Info("Hello Dagger 2")
    }
}
@Inject @field:Named("Love") lateinit var infoLove: Info
@Inject @field:Named("Hello") lateinit var infoHello: Info

Best practices

  • When we create a component, you should consider what element is responsible for the lifetime of that component
  • Use scoping only when it makes sense. Overusing scoping can have a negative effect on your app's runtime performance as objects stay in memory as long as the comnponent is in memory

Testing

Unit tests

You don't have to use Dagger for unit tests. when testing a class that uses contructor injection, you can directly call its constructor passing in fake or mock dependencies directly.

End-to-End tests

A good practice is to create a TestApplicationComponent meant for testing. Production code and testing code use different component configurations. You also need a TestApplication class that creates the TestApplicationComponent instead of an ApplicationComponent. Then, this test application is uses in a custom TestRunner that you'll use to run your instrumentation tests.

class CustomTestRunner : AndroidJUnitRunner() {

    override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
        return super.newApplication(cl, TestTodoApplication::class.java.name, context)
    }
}

In your app/gradle.build set up your custom test runner

android {
    .
    .
    .
    defaultConfig {
        .
        .
        .
        testInstrumentationRunner "<YOUR_CUSTOM_TEST_RUNNER>"
    }
}

References

Hope you liked it!

🐦 @LucianoSerruya

📧 lucianoserruya (at) gmail (dot) com