Dagger Party Tricks: Private 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:
@Module
object NetworkModule {
@Provides fun client(): OkHttpClient {
//...
}
@Provides fun retrofit(client: Lazy<OkHttpClient>): Retrofit {
//...
}
}
client
should be lazy!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.
@Retention(BINARY)
@Qualifier
private annotation class InternalApi
@Module
object NetworkModule {
@Provides
@InternalApi
fun provideClient(): OkHttpClient {
//...
}
@Provides
fun provideRetrofit(
@InternalApi client: Lazy<OkHttpClient>
): Retrofit {
//...
}
}
@Module
public abstract class NetworkModule {
@Retention(CLASS)
@Qualifier
private @interface InternalApi {}
@Provides
@InternalApi
static OkHttpClient provideClient() {
//...
}
@Provides
static Retrofit provideRetrofit(
@InternalApi Lazy<OkHttpClient> client
) {
//...
}
}
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!
@Component(modules = [NetworkModule::class])
interface FeatureComponent {
// This no longer compiles.
// Dagger will fail and say it can't find a matching dependency
fun client(): OkHttpClient
@InternalApi // <-- Won't compile!
fun internalClient(): OkHttpClient
}
@Component(modules = [NetworkModule::class])
interface FeatureComponent {
fun inject(controller: FeatureController)
}
class FeatureController @Inject constructor(
// This no longer compiles.
// Dagger will fail and say it can't find a matching dependency
val client: OkHttpClient,
@InternalApi // <-- Won't compile!
val internalClient: OkHttpClient
)
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