Dagger 2 for Android, Part VI ー @Component.Builder and @BindsInstance
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
.
@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
.
- Dagger 2 for Android, Part I ー What is Dependency Injection?
- Dagger 2 for Android, Part II ー The Basic Usage
- Dagger 2 for Android, Part III ー The @Qualifier and @Named Annotation
- Dagger 2 for Android, Part IV ー The @Scope Annotation
- Dagger 2 for Android, Part V ー @Inject for Constructor Injection
- Dagger 2 for Android, Part VI ー @Component.Builder and @BindsInstance (you are here)
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:
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
:
+ @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
:
@Singleton
@Component(modules = [DatabaseModule::class])
interface AppComponent {
fun inject(activity: MainActivity)
}
In 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:
@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:
@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:
@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
:
@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:
@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 👇:
@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:
@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. 👋
Clap to support the author, help others find it, and make your opinion count.