Dagger Party Tricks: Private Dependencies
Leveraging Dagger qualifiers to hide intermediate dependencies.
This is part 2 of a blog series of a talk I gave at Droidcon NYC and Londroid earlier this year. Dagger has a steep learning curve that frequently leaves developers not wanting to explore it further once they've integrated it. This series attempts to show some neat "party tricks" and other clever Dagger patterns to inspire more advanced usage.
The Problem
Consider the following Dagger module:
For all intents and purposes, the OkHttpClient
being provided here is just an implementation detail of the Retrofit
provider. NetworkModule
might be included in several other feature modules though, and consumers could (unintentionally or unscrupulously) ask for that client for their own purposes. That is to say, I could write another module like this and Dagger would happily comply:
@Module(includes = [NetworkModule::class])
object FeatureModule {
@Provides
fun networkAccessor(client: OkHttpClient): NetworkAccessor {
// ಠ_ಠ
}
}
In a way, this breaks encapsulation. You effectively want NetworkModule
to have a limited "public API" of sorts and just expose exactly what you want. However, everything in a module effectively has the same visibility. Not only this, but Dagger only generates public
factories for modules. How can we address this?
The Solution
Dagger has a notion of qualifiers. In short - these are custom annotations that are in turn annotated with @Qualifier
, used to indicate added metadata context. In Dagger, qualifiers are considered part of the type signature. That is to say - String
and @MyCustomQualifier String
are considered to be two distinct dependencies as far as Dagger is concerned.
What do qualifiers have to do with this? Private qualifiers! Dagger may generate public-everything, but we can qualify these private dependencies with qualifier annotations that aren't visible outside of our module.
We can use these to guard access to private intermediate dependencies. The compiler actually enforces this visibility for us too. As mentioned above, Dagger now sees OkHttpClient
as @InternalApi OkHttpClient
. Since @InternalApi
is private, no one downstream could write this:
@Module(includes = [NetworkModule::class])
object FeatureModule {
@Provides
@InternalApi // <-- Won't compile!
fun networkAccessor(client: OkHttpClient): NetworkAccessor {
// ಠ_ಠ
}
}
Other Benefits
This helps avoid accidentally using a dependency you don't own
@Module(includes = [NetworkModule::class])
object FeatureModule {
// This no longer compiles.
// Dagger will fail and say it can't find a matching dependency
@Provides
fun networkAccessor(client: OkHttpClient): NetworkAccessor {
// ಠ_ಠ
}
}
By extension, this also applies to all other consumer mechanisms too!
Hope this is useful! It's definitely the kind of thing that's usually only a problem in larger/distributed codebases, but it's also a good hygiene tactic to prevent dependencies from leaking.
If you want some real examples in action, I use this in my CatchUp side project:
DesignerNewsService
and its moduleMediumService
and its module