Dagger + Anvil: Learning to Love Dependency Injection on Android
The Android ecosystem has a healthy array of options for dependency injection. These days, the most common choices are Dagger (alone, with Hilt, or with Anvil), Koin, Kodein, and doing it by hand. Each option has its own set of pros and cons. At Tonal, we use Dagger with Anvil and it has been working so well that we think that it would be valuable to share our learnings with the Android community.
What is Anvil?
Historically, dependency injection frameworks tradeoff complexity and boilerplate for powerful features and type safety. When used alone, Dagger can be complex to set up, learn, and maintain. However, Anvil makes Dagger more powerful yet easier to use and teach to your team.
- Simple: nearly all of the complexity, learning, and maintenance overhead for the vast majority of day-to-day tasks nearly disappears.
- Modularization and dev apps: Anvil has become instrumental in building a highly modularized codebase and enabling dev apps that compile only a subset of our code.
- Build time: build times went down due to Anvil’s compiler plugin replacing Dagger’s kapt plugin. Anvil has more info on its README.
- Custom plugins: plugins can be written to remove boilerplate in ways that are specific to our codebase.
- No gotchas: we never hit any walls or gotchas because Anvil’s functionality is a complete superset of Dagger.
These benefits of Anvil come with all of the things that make Dagger great:
- Compile-time safety: you will never get a runtime crash due to a missing dependency.
- Powerful: Dagger is full of functionality such as multibinding that you can learn and incorporate over time as you need.
- Community: Dagger has a large and active community.
Simple Use Case
Let’s write some code that fetches the user’s current location and then fetches the weather.
So far, this looks the same as Dagger. Notice that you don’t need to declare your dependencies in modules. You can @Inject
your constructors directly with Dagger or any of its derivatives. In this example, @Singleton
is also used to indicate that the same instance of LocationProvider
should be provided to all other dependencies that inject it.
Easy Interface/Impl Modularization
Most large Android codebases are split up into multiple gradle modules. In a modularized codebase, it is common to have shared objects between modules.
The simple way would be to put all shared code in a library module that other modules can depend on. However, this has a couple of drawbacks:
- The full implementation of shared code must be compiled before anything that depends on it.
- Often, when working on the shared code, all dependent modules must be recompiled. This cost is paid both when writing code and also when rebasing new changes that have landed on
main
ormaster
.
The preferable way to handle this is to split up an object into an interface and an implementation. This improves build times, code isolation, and testability.
Our LocationProvider above would become:
Binding this implementation to the dependency with most frameworks is either challenging or requires frustrating boilerplate. With Anvil, it becomes as simple as this:
Adding @ContributesBinding
and a constructor with @Inject
is all you need. Anvil will take that annotation and generate a Dagger module that provides the annotated class as the implementation for the interface it implements. Modularizing a large codebase with Anvil has never been easier.
No Global List of Modules
One of the modularization challenges with Dagger is that your component has to know about everything that it needs to collect. For example, vanilla Dagger components must declare a list of all Modules that it should include.
Anvil inverts this dependency and is the root behind all of its @Contributes*
annotations. Feature modules contribute their own features up the tree to their component.
To clarify why this is useful, imagine a large application with hundreds of Gradle modules. Your team wants to create development apps that only compile a subset of features to cut down on build times and build convenient developer tools.
Vanilla Dagger
Whenever you create a new dependency, you must declare it in a Module. That Module must be included in a list in AppComponent
in the application module. However, you must then go to all other AppComponents
in the other application modules, determine whether that application module depends on your feature, and add it to that list as well. This can be time-consuming and error-prone as your team scales.
Anvil
With Anvil, you simply annotate your new dependency with @ContributesTo(AppComponent::class)
and your AppComponent
will automatically inherit all contributed dependencies without explicitly specifying them at the AppComponent
level. You don’t even have to create a Dagger Module!
Multibinding
Multibinding is a relatively advanced Dagger feature. It lets multiple features contribute to a single Set
or Map
which can then be injected by other classes. One example we use at Tonal is a set of custom json adapters for Moshi. We have a single Moshi instance for all of our API calls. However, individual features may need to contribute json adapters for specific APIs. Instead of domain-specific logic existing when we create our Moshi instance, each feature can contribute its own json adapter like this:
Then, our Moshi instance injects Set<CustomJsonAdapter>
and adds them all as custom adapters.
Some other use cases we use this for are:
- Running code at app startup.
- Running code when you start a workout.
- Running code on login/logout/remove account.
- Allowing features to provide their own feature flags.
Custom Plugins
Custom plugins are one of the most unique and powerful features of Anvil. Custom plugins allow you to create annotations that can then generate Dagger boilerplate for you. Although they do require more thought and upfront work, they have proven to be the most impactful changes we have made.
Custom plugins are also the least obvious because they are open-ended and they require understanding Anvil and Dagger well enough to map them to improvements you can make to your own codebase. To help with that, I’ll walk you through a couple of the custom plugins we have made at Tonal:
Before Anvil, creating a retrofit API required creating a Dagger module with a method that injected Retrofit then created and returned the API. Now, you can just annotate your retrofit API with @ContributesUserApi
or @ContributesNonUserApi
and that’s it! Anvil will do the rest for you.
Before Anvil, injecting dependencies into a ViewModel also required a Dagger Module with several lines of boilerplate split between the ViewModel and Module. Now, you can just annotate your ViewModel with @ContributesViewModel(AppComponent::class)
and Anvil will also do the rest for you.
Finally, Anvil allows us to create a type-safe way to launch one feature from another in a different module.
We have several more internal plugins, they just won’t make as much outside of the context of our codebase.
Anvil plugins aren’t 100% portable because they often rely on the internal structure of code. However, I’m providing our @Contributes*Api
and @ContributesViewModel
plugins as references here.
The upfront work to learn and build these plugins has more than paid off. It’s really incredible how much of a joy it is to use these features once they exist.
Setting up Anvil
A common argument against using dependency injection is that it isn’t worth the upfront investment, especially for small or hobby projects. However, with Anvil, you can get up and running with just a handful of lines of code.
First, create an interface that will represent your component. This should be in a shared module so all features can access it. In the examples above, you can see how AppComponent
is used in the parameter of most of Anvil’s annotations.
Then, create your @MergeComponent
. A MergeComponent is where Anvil will actually create the implementation of your component. In Vanilla Dagger, you will often have to revisit your component to add things like new Modules. However, because Anvil contributes dependencies directly from the location they are declared, after setting up the component, you will never have to revisit it.
Finally, instantiate your MergeComponent
.
Remove KAPT and Improve Build Time
Anvil comes with the ability to generate Dagger factories in each module it is used. Since switching to Anvil, we have removed Dagger’s KAPT processor from every single module except our application module. This has been a huge win for build times. Square has benchmarked the impact for them and outlined the benefits here.
What about Hilt?
The single biggest showstopper with Hilt that pushed us towards Anvil is its strict component hierarchy. Hilt enforces the following component structure.
This strict component structure works great for simple apps but can easily break down. I’ve annotated two examples (in red) of structures we use at Tonal that would have caused us problems with Hilt.
- You can log in and out of your account on Tonal so we have a
UserComponent
that is separate from ourSingletonComponent
. Hilt does not provide a way to inject aUserComponent
in betweenSingletonComponent
and anything below it. - We have a single activity. However, large features, such as our workout experience, are hosted by a single parent fragment with child fragments and ViewModels. We create a
WorkoutComponent
in the parentWorkoutFragment
for workout-scoped objects and need our workout ViewModels to have access toWorkoutComponent
. Again, Hilt ViewModelComponents are children ofActivityRetainedComponent
which has too large of a scope. We need it to be a child ofFragmentComponent
FragmentComponent
from the above bullet isn’t quite right, either. It must be aFragmentRetainedComponent
so that it survives configuration changes and has the same lifecycle as ViewModels. We handle that by using a special wrapper ViewModel that has the correct lifecycle. A snippet of our code can be found here.
These structures become major challenges because Hilt is a subset of Dagger while Anvil is a superset. Anvil is just as easy to use as Hilt for day-to-day tasks. However, it comes the ability to fully customize your component hierarchy. Anvil has recently further improved subcomponent support with @MergeSubcomponent
.
In addition, Hilt’s Gradle plugin consistently runs into conflicts with other dependencies (example 1, example 2) whereas Anvil’s Kotlin compiler plugin is fast and we have had zero issues with it so far.
What about [INSERT YOUR FAVORITE DI HERE]?
There are many ways to do dependency injection. None of them are wrong but they all have their own sets of pros and cons. I haven’t personally done manual dependency injection or used other frameworks such as Koin for anything more than hobby projects. I don’t feel qualified to discredit them just because there is something else that I like.
Setting up and using dependency injection can be intimidating. However, with the right set of tools and setup, they can be a joy to use and give you more time and freedom back to focus on building amazing products.
If there’s something else that you’d like to know or see examples of, let me know on Twitter and I can turn this into a series.
If you appreciate Anvil, make sure to give Ralf Wondratschek a shout-out, too! Open source projects are often published under the umbrella of a company but owned and maintained by a single person who often dedicates time on nights, weekends, and even after leaving the company!