Dagger 2 for Android, Part VI ー @Component.Builder and @BindsInstance

Tan Jun Rong avatar
Tan Jun Rong

In earlier series of my Dagger posts, we have looked at how we can inject dependencies into classes with Dagger 2. We need to add @Modules with @Provides method, then include them into the @Component.

For example, here is a simple module providing Taco.

TacoModule.kt
@Module class TacoModule { @Provides fun provideTaco(): Taco = Taco() }

I have to create an instance of Taco by calling it's constructor Taco.

However, we are not always able to instantiate the dependencies. For example, in Android development, context which comes from the Activity lifecycle cannot be instantiated. Hence, we need to pass it into Dagger when we get hold of it.

In this post, let's look at how it can be done using @Component.Builder and @BindsInstance.

Photo by shiangling from Unsplash
Photo by shiangling from Unsplash

The code example used in this article will be written in Kotlin for Android development.

This article requires basic knowledge of Dagger2 to understand.


Trying to inject Room Database which requires Context

Let's say we need to create a Room database instance, without Dagger2 injection, it would look like this:

MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val context = this // for illustration purpose val database = Room.databaseBuilder(context, MyDatabase::class.java,"MyDatabase") .build() }

Notice that Room.databaseBuidler is taking context as a dependency, so if we were to inject using dagger, context will not be something that we can 'create'.

Let's try it out and we will see an error:

In MainActivity.kt:

MainActivity.kt
+ @Inject + lateinit var database: MyDatabase override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val context = this // for illustration purpose - val database = Room.databaseBuilder(context, MyDatabase::class.java,"MyDatabase") - .build() + DaggerAppComponent.builder() + .build() + .inject(this) }

In AppComponent.kt:

AppComponent.kt
@Singleton @Component(modules = [DatabaseModule::class]) interface AppComponent { fun inject(activity: MainActivity) }

In DatabaseModule.kt:

DatabaseModule.kt
@Module class DatabaseModule { @Provides fun provideDatabase(): MyDatabase { return Room.databaseBuilder(context, MyDatabase::class.java,"MyDatabase") .build() } }

We can quickly spot that context will cause a syntax error ⚠️. ☝️

Solving without @BindsInstance

This can be fixed by adding the context as a depedency into the constructor of the module:

DatabaseModule.kt
@Module - class DatabaseModule { + class DatabaseModule(val context: Context) { @Provides fun provideDatabase(): MyDatabase { return Room.databaseBuilder(context, MyDatabase::class.java,"MyDatabase") .build() } }

In our injection side, we have to make a change too:

MainActivity.kt
@Inject lateinit var database: MyDatabase override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val context = this DaggerAppComponent.builder() + .databaseModule(DatabaseModule(context)) .build() .inject(this) }

This way, we can manually make an instance of DatabaseModule, and pass in the context through it's constructor.

While this method will work, there will be a lot of boilerplate code if we require context in many modules, for example 👇:

      DaggerAppComponent.builder()
          .databaseModule(DatabaseModule(context))
+          .someModule1(SomeModule1(context))
+          .someModule2(SomeModule2(context))
+          .someModule3(SomeModule3(context))
           .build()
           .inject(this)

There's where @BindsInstance will help!

@Component.Builder and @BindsInstance to the rescue!

Let's re-write this by using @Component.Builder and @BindsInstance. Using this 2 annotations, we can pass in context using just once and let Dagger help us supply context everything any modules need it.

First, we will remove the context from the constructor and add it as an argument to the provide method:

DatabaseModule.kt
@Module - class DatabaseModule(val context: Context) { + class DatabaseModule { @Provides - fun provideDatabase(): MyDatabase { + fun provideDatabase(context: Context): MyDatabase { return Room.databaseBuilder(context, MyDatabase::class.java,"MyDatabase") .build() } }

If we try to compile at this point, we will get an error:

error: [Dagger/MissingBinding] android.content.Context cannot be provided without an @Provides-annotated method.

This is because we haven't passed in context into Dagger.

Let's make some changes in AppComponent.kt:

AppComponent.kt
@Singleton @Component(modules = [DatabaseModule::class]) interface AppComponent { fun inject(activity: MainActivity) + @Component.Builder + abstract class Builder { + @BindsInstance + abstract fun bindContext(context: Context): Builder + abstract fun build(): AppComponent + } }

By adding this builder inner abstract class, with bindContext(context) method, Dagger will generate bindContext method for us to pass it in using the builder.

In MainActivity, we can do this now:

MainActivity.kt
@Inject lateinit var database: MyDatabase override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val context = this DaggerAppComponent.builder() - .databaseModule(DatabaseModule(context)) + .bindContext(context) .build() .inject(this) }

Now the provide method will auto-magically received context from Dagger!

This will now pass 👇:

DatabaseModule.kt
@Module class DatabaseModule { @Provides fun provideDatabase(context: Context): MyDatabase { return Room.databaseBuilder(context, MyDatabase::class.java,"MyDatabase") .build() } }

Documentation

It's important to go through the details of the syntax for @Component.Builder and the documentation to understand how to adapt it into other use cases.

I'm curious as to why there is always a method that returns the component in all the examples I can find:

@Component.Builder
interface Builder {
    @BindsInstance
    fun bindContext(context: Context): Builder
+   fun build(): AppComponent <----- this fellow
}  

That's because it's stated in the documentation of @ComponentBuilder:

A builder is a type with setter methods for the modules, dependencies and bound instances required by the component and a single no-argument build method that creates a new component instance.


Then, I wondered why the builder has to be abstract. Can it be class or interface?

The answer is it can be an interface, but it cannot be a class.

This will work too:

AppComponent.kt
@Singleton @Component(modules = [DatabaseModule::class]) interface AppComponent { fun inject(activity: MainActivity) - @Component.Builder - abstract class Builder { - @BindsInstance - abstract fun bindContext(context: Context): Builder - abstract fun build(): AppComponent - } + @Component.Builder + interface Builder { + @BindsInstance + fun bindContext(context: Context): Builder + fun build(): AppComponent + } }

As stated in the @Component.Builder:

Components may have a single nested static abstract class or interface annotated with @Component.Builder.

There are more details in the documentation, feel free to read it!

Closing

We can use this method to pass in any arguments that are only available during runtime. Hope you find this post helpful. 👋