GameChanger and Anvil

Grant Park

At GameChanger, we have a dedicated App Platform team that focuses on scaling our engineering organization and supporting development teams across the company. Our mission is to make building things the right way the easy way for all developers at GameChanger. This involves:

  • Implementing and maintaining core infrastructure like dependency injection systems
  • Creating and improving development tools and workflows
  • Establishing best practices and architectural patterns
  • Providing support and guidance to feature teams

By having a dedicated team focus on these foundational aspects, we achieve several benefits:

  • Consistency across the codebase, making it easier for developers to switch between projects
  • Reduced cognitive load for feature teams, allowing them to focus on business logic
  • Faster onboarding for new team members
  • Improved code quality and maintainability

The work on integrating Anvil with our existing Dagger setup is a prime example of how the App Platform team contributes to these goals. By streamlining our dependency injection process, we’re making it easier for all teams to build features efficiently and correctly.

With this context in mind, let’s explore the technical details of our setup and why we chose to augment Dagger with Anvil.

What’s the setup?

image.png

The Android codebase has the following Dagger components: a longstanding Application Component, a User Component that lives for as long as the user is logged in, and Game Components that last for as long as any live game.

Features usually involve a fragment to render UI and a view model to manage business logic and state. Said view model is supplied by a factory in either the Application Component or User Component and pulls in dependencies from the rest of the Dagger graph that may come from any of the mentioned components.

So Why Anvil?

1. Anvil makes modularization easy:

image.png

We’re following, as shown in the diagram above, a format for modules across the codebase to follow that involve the inversion of control principle, similar to what’s described here.

  • :pub is for public facing interfaces and models
  • :impl contains implementations of said interfaces.

We don’t want any modules to depend on :impl and instead depend on :pub to ensure lean compilation times and exposure of only the bare necessities for every feature. To put it all together into an app, normally you would manually write Dagger modules to provide :impl contents for your :pub interfaces as visualized below. An orchestrating shell or app module can then use said wiring modules to facilitate the entire app.

image.png

With Anvil, however, we can get rid of manually writing Dagger modules for the wiring and simply use annotations like @ContributesTo to generate those modules instead, eliminating the need for writing the wiring altogether. In other words, Anvil makes DI easy. When DI is easy, modularization is easy.

2. We want to skip the Dagger boilerplate:

To ensure a view model is in the dependency graph, some boilerplate is required in the appropriate Dagger module (either Application or User):

@Binds @IntoMap @ViewModelKey(MyViewModel::class)
abstract fun bindMyViewModel(viewModel: MyViewModel): ViewModel

With Anvil, we make this go away by simply annotating the top of a view model with:

@ContributesMultibinding(UserScope::class)
@BindingKey(MyViewModel::class)
class MyViewModel @Inject constructor()

And with an additional custom annotation and some code generation using some handy code-gen hooks exposed by Anvil, the above can be further simplified to a single line like this:

@ContributesViewModel(UserScope::class)
class MyViewModel @Inject constructor()

Under the hood, we use this custom annotation along with the CodeGenerator API from Anvil (which we won’t dig into here) to generate a module with aggregated bindings collected from annotated view models like the above, which looks something like this:

@Module
@ContributesTo(MyAnvilScope::class)
abstract class MyViewModel_Module {
  @Binds @IntoMap @ViewModelKey(MyViewModel::class)
  abstract fun bindMyViewModel(viewModel: MyViewModel): ViewModel
  
  @Binds @IntoMap @ViewModelKey(MyViewModel2::class)
  abstract fun bindMyViewModel2(viewModel: MyViewModel2): ViewModel
  
  @Binds @IntoMap @ViewModelKey(MyViewModel3::class)
  abstract fun bindMyViewModel3(viewModel: MyViewModel3): ViewModel
  
  ...
}

This means that if you want to start a feature in our codebase, you simply create a fragment to navigate to and summon from the dagger graph a view model that you annotate with a single line.

What about other alternatives?

After considering Koin, ultimately dismissing it due to the amount of overhaul required on our existing Dagger code, we took a look at Hilt for potentially similar ergonomic gains across the codebase given that it’s built on top of Dagger.

However, we learned that Hilt requires an invasive amount of work as well. We would need to annotate every Android entry point (broadcast receivers, activities, fragments, etc.), and it is highly opinionated, making it less trivial to work with the custom Dagger components described in the beginning.

We found that Anvil required no extra overhead and could fit least invasively into our codebase with only a few addendums to existing patterns (like creating Anvil scopes) that ultimately allow developers to write less code.

Conclusion

Anvil on top of Dagger is our tool of choice for dependency injection. Its compatibility with our existing custom Dagger components and ability to generate wiring modules automatically make it superior to alternatives like Koin and Hilt for our specific needs.

By significantly reducing Dagger boilerplate, simplifying view model integration, and facilitating easier modularization, it has streamlined our dependency injection workflow; allowing us to easily build features the right way.