Dagger2 is hard, but it can be easy, part 6
In the previous post we’ve encountered subcomponents and their hierarchy, custom scopes and we did not waste any time.
In this post we’ll learn when’s the right place to inject, named and qualifiers.
Application’s correct place for injection
We’ve seen in the previous post when’s the right place to inject the @Singleton
scope, as a reminder it should be right before the super.onCreate in your : Application
inheritance
1
2
3
4
override fun onCreate() {
applicationComponent = DaggerSingletonComponent.factory().create(this).also { it.provideGraphInside(this) }
super.onCreate()
}
Fragment’s correct place for injection
Go ahead and create a Fragment subcomponent
1
2
3
4
5
6
7
8
9
10
11
@Subcomponent(modules = [FragmentModule::class])
@FragmentScoped
interface FragmentComponent {
@Subcomponent.Factory
interface Factory {
fun create(): FragmentComponent
}
fun inject(homeFragment: HomeFragment)
}
an empty Fragment module if you want to use later on (optional)
1
2
3
4
5
@Module
object FragmentModule {
}
a scope for our fragment
1
2
3
4
@Scope
@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME)
annotation class FragmentScoped
and add
1
fun fragmentComponentFactory(): FragmentComponent.Factory
in our SingletonComponent
right above/below your
1
fun activityComponentFactory(): ActivityComponent.Factory
a HomeFragment
class for demonstration
1
2
3
4
5
6
7
8
9
class HomeFragment : Fragment(R.layout.home_fragment) {
override fun onAttach(context: Context) {
injector {
fragmentComponentFactory().create().inject(this@HomeFragment)
}
super.onAttach(context)
}
}
as you can see, the correct place to inject a Fragment is in onAttach
since that’s called just once when the Fragment is attached and as an analogy destroyed when the fragment is detached.
Our injector
extension function changed a bit
1
2
3
4
5
6
7
inline fun FragmentActivity.injector(action: SingletonComponent.() -> Unit) {
(application as DaggerIsEasyApplication).applicationComponent.action()
}
inline fun Fragment.injector(action: SingletonComponent.() -> Unit) {
requireActivity().injector(action)
}
we changed from AppcompatActivity.injector
to FragmentActivity
so that we can reuse it for our Fragment.injector
which has a requireActivity()
that’s of a type FragmentActivity
.
Activity’s correct place for injection
When it comes to an Activity we can go one way or the other, in the previous post we’ve bound the instance of savedInstanceState
and intent
just to see the different approaches now.
- onCreate just before super.onCreate like in the
: Application
as seen in the previous post1 2 3 4 5 6
override fun onCreate(savedInstanceState: Bundle?) { injector { activityComponentFactory().create(savedInstanceState, intent).also { it.inject(this@MainActivity) } } super.onCreate(savedInstanceState) }
addOnContextAvailableListener
when it comes to this method we can have something like1 2 3 4 5 6 7 8 9 10
class MainActivity : AppCompatActivity() { init { addOnContextAvailableListener { injector { activityComponentFactory().create(savedInstanceState, intent).also { it.inject(this@MainActivity) } } } } }
When’s
addOnContextAvailableListener
called?
A
> If you do your injection insideonCreate(savedInstanceState: Bundle?)
beforesuper.onCreate(savedInstanceState)
then this is called beforeaddOnContextAvailableListener
has a context available.B
> If you do your injection insideaddOnContextAvailableListener
then this is done also beforesuper.onCreate(savedInstanceState)
but right afterA
Our example is tied to providing instances of savedInstanceState, intent
which shows us that it depends on your use case and how you created the whole logic.
Make sure you’re having the latest appCompat dependency to get the context listener, as of writing this blogpost, i’m using
1
implementation "androidx.appcompat:appcompat:1.3.0-rc01"
To correct our sample if we’re to use the injection in the context available listener we have to do the following
1
2
3
4
5
6
7
8
9
10
class MainActivity : AppCompatActivity() {
init {
addOnContextAvailableListener { availableContext ->
injector {
activityComponentFactory().create(availableContext).also { it.inject(this@MainActivity) }
}
}
}
}
and correct our ActivityComponent
’s create function to receive a Context
1
2
3
4
5
6
7
8
9
10
11
@Subcomponent(modules = [ActivityModule::class])
@ActivityScoped
interface ActivityComponent {
@Subcomponent.Factory
interface Factory {
fun create(@BindsInstance context: Context): ActivityComponent
}
fun inject(mainActivity: MainActivity)
}
then you press F10 and build the application, it crashes.
1
error: [Dagger/DuplicateBindings] android.content.Context is bound multiple times
That means we already have a Context
bound, when we built our SingletonComponent
, one way to fix this is
1
2
3
4
5
6
7
8
9
10
@Component
@Singleton
interface SingletonComponent {
@Component.Factory
interface SingletonComponentFactory {
fun create(@BindsInstance context: Application): SingletonComponent
}
fun activityComponentFactory() : ActivityComponent.Factory
fun provideGraphInside(application: DaggerIsEasyApplication)
}
but having @BindsInstance context: Application
means that we have to change every place where we have our application context to be of a type Application
.
The other one is to use a Qualifier or Named, these are more elegant solutions.
For this demonstration go ahead and delete Bundler
and IntentHandler
classes and remove them from everywhere we’ve used them so far, we won’t be needing them anymore.
Named
When we use named, we have to use @Named
which is an annotation that takes a string as an argument and you have to write the string yourself which can be prone to errors if you don’t expose it as a constant, meaning that writing one string at two places can be prone to a human error (typo etc..).
To avoid our Context
clash that we had, we have to use @Named
to the instance in our case @Named(APPLICATION_CONTEXT_NAME)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
@Singleton
interface SingletonComponent {
companion object {
const val APPLICATION_CONTEXT_NAME = "applicationContext"
}
@Component.Factory
interface SingletonComponentFactory {
fun create(@BindsInstance @Named(APPLICATION_CONTEXT_NAME) context: Context): SingletonComponent
}
fun activityComponentFactory(): ActivityComponent.Factory
fun fragmentComponentFactory(): FragmentComponent.Factory
fun provideGraphInside(application: DaggerIsEasyApplication)
}
and in our ActivityComponent
we have @Named(ACTIVITY_CONTEXT_NAME)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Subcomponent(modules = [ActivityModule::class])
@ActivityScoped
interface ActivityComponent {
companion object {
const val ACTIVITY_CONTEXT_NAME = "activityContext"
}
@Subcomponent.Factory
interface Factory {
fun create(@BindsInstance @Named(ACTIVITY_CONTEXT_NAME) context: Context): ActivityComponent
}
fun inject(mainActivity: MainActivity)
}
now we’ve used an Application
’s Context
in SharedPreferencesManager
the change that has to be done as well
1
2
3
4
5
6
@Singleton
class SharedPreferencesManager @Inject constructor(@Named(SingletonComponent.APPLICATION_CONTEXT_NAME) context: Context) {
.
.
.
}
@Named(SingletonComponent.APPLICATION_CONTEXT_NAME)
means that when Dagger comes into conflicts for the instances of the same type, it can provide them by a name we’ve explicitly instructed it to do.
Qualifier
The qualifier approach is something I personally prefer to use, because it requires of you as a developer to create a separate Qualifier
annotation and I can find it easily into the package where I use that module or class that’s using the qualifier or (your use case or preference here).
To create a @Qualifier
is really easy, just create a new annotation and add the @Qualifier
and @Retention(AnnotationRetention.BINARY)
Why BINARY
you might ask yourself, that’s because the annotation is stored in a binary form which is inaccessible for reflection.
Go ahead and create ApplicationContext
1
2
3
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class ApplicationContext
and respectively ActivityContext
1
2
3
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class ActivityContext
now our SingletonComponent
looks cleaner.
1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
@Singleton
interface SingletonComponent {
@Component.Factory
interface SingletonComponentFactory {
fun create(@BindsInstance @ApplicationContext context: Context): SingletonComponent
}
fun activityComponentFactory(): ActivityComponent.Factory
fun fragmentComponentFactory(): FragmentComponent.Factory
fun provideGraphInside(application: DaggerIsEasyApplication)
}
Make the change inside the ActivityComponent
1
2
3
4
5
6
7
8
9
10
11
@Subcomponent(modules = [ActivityModule::class])
@ActivityScoped
interface ActivityComponent {
@Subcomponent.Factory
interface Factory {
fun create(@BindsInstance @ActivityContext context: Context): ActivityComponent
}
fun inject(mainActivity: MainActivity)
}
now our SharedPreferenceManager
looks cleaner too!
1
2
3
4
5
6
@Singleton
class SharedPreferencesManager @Inject constructor(@ApplicationContext context: Context) {
.
.
.
}
This I see as an absolute win.
Broadcast receiver’s correct place for injection
- If you have a receiver that is coming from the
AndroidManifest.xml
, you can’t cast theContext
to an application context since it’s mostly of a typeReceiverRestrictedContext
, you can however create a separate graph.
1
2
3
4
5
6
7
8
9
10
11
@Component
@BroadcastScoped
interface BroadcastComponent {
@Component.Factory
interface Factory {
fun create(): BroadcastComponent
}
fun inject(bootReceiver: BootReceiver)
}
1
2
3
4
@Scope
@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME)
annotation class BroadcastScoped
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
//do the injection here
DaggerBroadcastComponent.create().inject(this)
// you can have create accept the context and intent as @BindsInstance etc...
intent ?: return
context ?: return
val action = intent.action ?: return
if (action == Intent.ACTION_BOOT_COMPLETED || action == Intent.ACTION_LOCKED_BOOT_COMPLETED) {
//some logic of your own
}
}
}
- Registering the receiver at runtime
For the sake of A.
and B.
create a class
1
2
3
4
5
6
7
class PowerLogger @Inject constructor(private val sharedPreferencesManager: SharedPreferencesManager) {
fun logChargingStatus(status: Boolean) {
sharedPreferencesManager.logChargingStatus(if (status) "IS CHARGING" else "CHARGING STOPPED")
}
}
and inside the sharedPreferenceManager
add a function
1
2
3
fun logChargingStatus(status: String) {
Log.d(this::class.java.simpleName, status)
}
and Subcomponent that’ll inherit from ApplicationComponent
1
2
3
4
5
6
7
8
9
10
@Subcomponent
interface RuntimeBroadcastComponent
{
@Subcomponent.Factory
interface Factory {
fun create(): RuntimeBroadcastComponent
}
fun inject(powerReceiver: PowerReceiver)
}
don’t forget to add
1
fun runtimeBroadcastComponentFactory() : RuntimeBroadcastComponent.Factory
inside the ApplicationComponent
just as you did for the Fragment and Activity, this is only needed for A.
A.
If you’re using field injection and manually registering the receiver (which is painful to do so, we’ll see simpler a bit below)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class PowerReceiver : BroadcastReceiver() {
@Inject
lateinit var powerLogger: PowerLogger
override fun onReceive(context: Context?, intent: Intent?) {
context ?: return
intent ?: return
(context.applicationContext as DaggerIsEasyApplication).applicationComponent.runtimeBroadcastComponentFactory().create().inject(this)
val isCharging = when (intent.action) {
Intent.ACTION_POWER_DISCONNECTED -> false
Intent.ACTION_POWER_CONNECTED -> true
else -> null
}
isCharging?.let { status ->
powerLogger.logChargingStatus(status)
}
}
}
inside MainActivity
1
2
3
4
val filter = IntentFilter(Intent.ACTION_POWER_CONNECTED).also {
it.addAction(Intent.ACTION_POWER_DISCONNECTED)
}
registerReceiver(PowerReceiver(),filter)
when the power connects/disconnects we have the following
B.
If you’re manually registering the receiver then you can use constructor injection to get an instance of your receiver and register it with that instance, since you’re using
1
registerReceiver(broadcastReceiver, filter)
which takes a broadcastReceiver
instance and an intent filter.
If you’re using this approach you can go ahead and remove the RuntimeBroadcastComponent
and remove it as a subcomponent from the ApplicationComponent
also change PowerReceiver
to
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class PowerReceiver @Inject constructor(private val powerLogger: PowerLogger) : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
context ?: return
intent ?: return
val isCharging = when (intent.action) {
Intent.ACTION_POWER_DISCONNECTED -> false
Intent.ACTION_POWER_CONNECTED -> true
else -> null
}
isCharging?.let { status ->
powerLogger.logChargingStatus(status)
}
}
}
and MainActivity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class MainActivity : AppCompatActivity() {
init {
addOnContextAvailableListener { availableContext->
injector {
activityComponentFactory().create(availableContext).also { it.inject(this@MainActivity) }
}
}
}
@Inject
lateinit var powerReceiver: PowerReceiver
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val filter = IntentFilter(Intent.ACTION_POWER_CONNECTED).also {
it.addAction(Intent.ACTION_POWER_DISCONNECTED)
}
registerReceiver(powerReceiver,filter)
}
}
As you can see this is easier approach and as you’ve read in the beginning, always prefer constructor injection whenever possible.
Service’s correct place for injection
By now you know how to create a subcomponent or a component, you can get the context from the service, since you’ve started that service from an Activity/Fragment or a Context, that can be easily casted to applicationContext
therefore easily creating a subcomponent of the ApplicationComponent
or if your use case is different you’ll know soon enough where to create your graph unrelated to ApplicationComponent
.
The correct place for the Service to be injected is inside onCreate()
right before the super.onCreate()
call, just as our : Application
1
2
3
4
5
6
7
8
9
class LongRunningService : Service() {
override fun onBind(intent: Intent?): IBinder? = null
override fun onCreate() {
//the correct place to inject
super.onCreate()
}
}
Work manager
Let’s say we have a Worker class
1
2
3
4
class ContentRecommendation(private val appContext: Context,
params: WorkerParameters,
private val defaultPrefs: SharedPreferences,
private val contentRecommendationAPI: ContentRecommendationAPI) : CoroutineWorker(appContext, params)
for this to work we need to create a WorkerFactory that creates ListenableWorker instances, the factory is invoked every time a work runs.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ContentFactory(private val defaultPrefs: SharedPreferences,
private val contentRecommendationAPI: ContentRecommendationAPI
) : WorkerFactory() {
override fun createWorker(appContext: Context, workerClassName: String, workerParameters: WorkerParameters): ListenableWorker? {
return when (workerClassName) {
ContentRecommendation::class.java.name ->
ContentRecommendation(appContext, workerParameters, defaultPrefs, contentRecommendationAPI)
else ->
// Return null, so that the base class can delegate to the default WorkerFactory.
null
}
}
}
then we also need the DelegatingWorkerFactory (injection factory) which delegates to other factories
1
2
3
4
5
6
7
8
9
@Singleton
class ContentRecommendationFactory @Inject constructor(
defaultPrefs: SharedPreferences,
contentRecommendationAPI: ContentRecommendationAPI
) : DelegatingWorkerFactory() {
init {
addFactory(ContentFactory(defaultPrefs, contentRecommendationAPI))
}
}
then inside : Application
you have to implement Configuration.Provider
and have something like
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class WorkerApplicationDemonstration : Application(), Configuration.Provider {
@Inject
lateinit var contentRecommendationFactory: ContentRecommendationFactory
@Inject
lateinit var contentConfiguration: Configuration
override fun getWorkManagerConfiguration() = contentConfiguration
lateinit var appComponent: AppComponent
override fun onCreate() {
super.onCreate()
appComponent = DaggerAppComponent.factory().create(this).also { it.inject(this) }
WorkManager.initialize(this, contentConfiguration)
}
}
You think it’s over? Nope…
Since we’ve created a custom factory, we have to disable (remove) the default initialization factory or ours won’t run.
Inside your AndroidManifest.xml
1
2
3
4
5
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
android:exported="false"
tools:node="remove" />
What’s next?
Probably everyone’s wondering, why’s there no blog post about Hilt since it’s easier, yes Hilt is easier because it does all of this for you behind the scenes so that you don’t have to do it, there’s one more blog post about the dreaded but powerful Multi bindings
, I won’t cover WorkManager’s injection using this example but you saw how much boilerplate we have to write and after the Multi bindings
post we’re jumping to the Hilt train, which offers easier integration with WorkManager
and with practically everything, not just Workmanager
.