Escaping dagger-android With a Custom Anvil Generator
Slack’s Android app relies heavily on Dagger for compile-time dependency injection. While Dagger is powerful, it brings along boilerplate in the form of modules and components. Anvil steps in to generate that plumbing automatically, and its newest compiler-api artifact lets us push even further. In particular, we used it to replace our legacy dagger-android usage with a lightweight, custom solution that fits our scoping model exactly.
Why We Needed a Custom Pattern
Like many Android codebases, we historically used @ContributesAndroidInjector from the now-deprecated dagger-android library to wire activities. Rather than migrate to a new opinionated framework like Hilt, we decided to stay on low-level Dagger APIs and build what we needed ourselves. Activities are a special case in Dagger because they require member injection and need something else to trigger the injection in onCreate(). Doing that by hand across all our screens would add repetitive, error-prone inject() methods to our components.
Our scope hierarchy complicates matters further. We have three main scopes: an app-wide scope, an org scope (for an individual organization containing multiple workspaces), and a user scope (for each individual workspace).
Anatomy of Our Solution
We set strict requirements for the replacement: minimal boilerplate, support for targeting the right scope, zero reliance on dagger-compiler/dagger-android-processor (to reduce kapt usage), and no reflection. Our approach hinges on a Dagger intrinsic: requesting a class’s MembersInjector instance directly. That’s not just handy; Dagger will fulfill such requests automatically.
Each activity gets an associated type that pulls its MembersInjector from Dagger:
@InjectWith(UserScope::class)
class ChannelInfoActivity : BaseActivity {
@Inject lateinit var presenter: ChannelInfoActivityPresenter
// ...
}
The generated Dagger wiring for this looks like:
class ChannelInfoActivityAnvilInjector @Inject constructor(
override val injector: MembersInjector<ChannelInfoActivity>
) : AnvilInjector<ChannelInfoActivity>
@Module
@ContributesTo(UserScope::class)
interface ChannelInfoActivityAnvilInjectorBinder {
@IntoMap
@Binds
@ActivityKey(ChannelInfoActivity::class)
fun ChannelInfoActivityAnvilInjector.bind(): AnvilInjector<*>
}
Collecting all these injectors in a map avoids long lists of named inject() functions. This is done by having small “binder” types register to a host component’s multibinding:
interface AnvilInjector<T> {
val injector: MembersInjector<T>
fun inject(target: T) {
injector.injectMembers(target)
}
}
interface UserComponent {
// ...
fun activityInjectors(): Map<Class<out BaseActivity>, AnvilInjector<*>>
}
Then in our BaseActivity, onCreate() simply pulls this map and uses the correct injector on the activity instance:
private fun anvilInject() {
// Read the InjectWith annotation's scope value
val scope = ...
if (scope == UserScope::class) {
// Can't completely escape raw application access can we,
// Android?
val injector = application.appComponent()
.userComponentFor(userId)
.activityInjectors()[javaClass] // the injector
.inject(this)
}
}
This design meets all requirements except one: it still creates boilerplate — an inventor class and binding module per activity. That’s where Anvil comes in.
Removing the Remaining Boilerplate
The two per-activity types are entirely mechanical. From the activity type and its scope, we have all the info needed to generate them. Anvil’s compiler API enables this: implementing a CodeGenerator allows us to inspect the source code and emit what we need at compile time whenever we encounter our own annotation, @InjectWith.
@AutoService(CodeGenerator::class)
class AnvilInjectorGenerator : CodeGenerator {
override fun isApplicable(context: AnvilContext): Boolean {
// Behavior is not dependent on factory generation
return true
}
override fun generateCode(
codeGenDir: File,
module: ModuleDescriptor,
projectFiles: Collection<KtFile>
): Collection<GeneratedFile> {
// Process projectFiles
// Filter on classes annotated with @InjectWith
// Read the scope type and qualified class name
// Generate the AnvilInjector file with KotlinPoet
// Generate the InjectorBinder file with KotlinPoet
return generatedFiles
}
}
Our generator is an ordinary local Gradle subproject. Consuming modules add it via the anvil() configuration. The implementation builds on the same PSI infrastructure you’d use with custom Android lint rules; Anvil’s own MembersInjectorGenerator is a useful reference for the level of complexity involved.
plugins {
// ...
id("com.squareup.anvil")
}
dependencies {
anvil(":our:anvil:code-generator-project")
}
With this generator in place, enabling injection for a new activity is literally one line:
@InjectWith(UserScope::class)
class ChannelInfoActivity : BaseActivity {
@Inject lateinit var presenter: ChannelInfoActivityPresenter
// ...
}
Broader Implications
While activities were the starting point, the infrastructure generalizes to any member-injected type, including services and jobs. And because this code is generated ahead of Dagger’s own processing, we can remove dagger-android-processor entirely — no kapt increase involved. The approach also supports adding extra lint checks; for instance, activities bound to org or user scope must implement an internal interface that retrieves the logged-in user, which we enforce automatically.



