$30 off During Our Annual Pro Sale. View details »

Dagger + Anvil: Learning to Love Dependency Injection

Ralf
June 03, 2022

Dagger + Anvil: Learning to Love Dependency Injection

This is a joint talk with Ralf Wondratschek from Square and Gabriel Peal from Tonal.

Anvil is a Kotlin compiler plugin that makes dependency injection with Dagger 2 easier. Anvil reduces boilerplate code, improves code modularization, reduces build times, and enables custom code generators to further simplify patterns specific to your codebase.

In this talk we will explain why Square created Anvil, how Tonal successfully adopted it, how the plugin works under the hood, what code is being generated and how you can get the most out of the framework.

Ralf

June 03, 2022
Tweet

More Decks by Ralf

Other Decks in Programming

Transcript

  1. Dagger + Anvil Learning to Love Dependency Injection Ralf Wondratschek

    @vRallev Gabriel Peal @gpeal8
  2. Dagger 2 Refresher - Basic @Singleton @Component interface AppComponent class

    MainApplication : Application() { lateinit var component : AppComponent override fun onCreate () { super.onCreate() component = DaggerAppComponent.create() } } :app @Singleton class WeatherRepository @Inject constructor() { suspend fun getForecast(): List<Int> = TODO() } :feature.weather
  3. Dagger 2 Refresher - Interface + Impl @Singleton @Component(modules =

    [WeatherModule::class]) interface AppComponent interface WeatherRepository { suspend fun getForecast(): List<Int> } :lib.weather @Singleton class WeatherRepositoryImpl @Inject constructor () : WeatherRepository { override suspend fun getForecast (): List<Int> = TODO() } @Module abstract class WeatherModule { @Binds abstract fun bindsWeatherRepository (r: WeatherRepositoryImpl ): WeatherRepository } :app :feature.weather
  4. Dagger 2 Refresher - Binding Interface @Singleton @Component interface AppComponent

    : WeatherBindings interface WeatherBindings { fun inject(fragment: WeatherFragment) fun weatherRepository(): WeatherRepository } :app :feature.weather
  5. Dagger 2 Refresher - Multibindings @Singleton @Component interface AppComponent :app

    interface WeatherDataSource { suspend fun getForecast (): List<Int> } class WeatherChannelDataSource @Inject constructor () : WeatherDataSource { override suspend fun getForecast (): List<Int> = TODO() } class WeatherUndergroundDataSource @Inject constructor (): WeatherDataSource { override suspend fun getForecast (): List<Int> = TODO() } @Singleton class WeatherRepository @Inject constructor ( private val userPrefs: UserPrefs, private val dataSources : Map<String, WeatherDataSource>, ) { suspend fun getForecast (): List<Int> = TODO() } @Module abstract class WeatherModule { @Binds @IntoMap @StringKey ("WeatherChannel" ) abstract fun bindsWeatherChannel (s: WeatherChannelDataSource ): WeatherDataSource @Binds @IntoMap @StringKey ("WeatherUnderground" ) abstract fun bindsWeatherUnderground (s: WeatherUndergroundDataSource ): WeatherDataSource } :feature.weather (modules = [WeatherModule::class])
  6. Dagger 2 • Why it’s good ◦ Build time guarantee

    ◦ Fast at runtime (no reflection) ◦ Huge community (Stack Overflow/Blog posts galore) ◦ You can get it to do just about anything if you need it
  7. Dagger 2 • Why it’s not so good ◦ Boilerplate

    ◦ Complicated to learn (names aren’t great) ◦ Build time (kapt)
  8. Why Anvil? • Opinionated module structure • Hundreds of demo

    applications → Boilerplate code became a problem
  9. Why Anvil? :app.1 @Component( WeatherModule, LocationModule ) AppComponent :app.2 @Component(

    LocationModule, UserModule ) AppComponent :app.3 @Component( LocationModule, UserModule ) AppComponent :feature.1 WeatherModule :feature.2 LocationModule :feature.3 UserModule
  10. Why Anvil? Try to build :demo app Add missing dependencies

    in build.gradle file Sync :demo module in Android Studio Add Dagger modules to Dagger components
  11. Why Anvil? Try to build :demo app Add missing dependencies

    in build.gradle file Sync :demo module in Android Studio Add Dagger modules to Dagger components
  12. Why Anvil? @Singleton @Component(modules = [WeatherModule::class]) interface AppComponent

  13. Why Anvil? @Singleton @Component(modules = [WeatherModule::class]) interface AppComponent

  14. Why Anvil? • A Dagger module must know to which

    component it belongs • A Dagger component must know which modules it needs to include → The scope is the shared connection
  15. Why Anvil? @Component(modules = [WeatherModule::class]) interface AppComponent @Module object WeatherModule

  16. Why Anvil? @MergeComponent(AppScope::class) interface AppComponent @Module @ContributesTo(AppScope::class) object WeatherModule

  17. Why Anvil? @MergeComponent(AppScope::class) interface AppComponent @Module @ContributesTo(AppScope::class) object WeatherModule

  18. Why Anvil? :app.1 @Component( WeatherModule, LocationModule ) AppComponent :app.2 @Component(

    LocationModule, UserModule ) AppComponent :app.3 @Component( LocationModule, UserModule ) AppComponent :feature.1 WeatherModule :feature.2 LocationModule :feature.3 UserModule
  19. Why Anvil? :app.1 @MergeComponent(AppScope) AppComponent :app.2 @MergeComponent(AppScope) AppComponent :app.3 @MergeComponent(AppScope)

    AppComponent :feature.1 @ContributesTo(AppScope) WeatherModule :feature.2 @ContributesTo(AppScope) LocationModule :feature.3 @ContributesTo(AppScope) UserModule
  20. Why Anvil? :app.1 @MergeComponent(AppScope) AppComponent :app.2 @MergeComponent(AppScope) AppComponent :app.3 @MergeComponent(AppScope)

    AppComponent :feature.1 @ContributesTo(AppScope) WeatherModule :feature.2 @ContributesTo(AppScope) LocationModule :feature.3 @ContributesTo(AppScope) UserModule :core object AppScope
  21. Why Anvil? • Allows you to construct the dependency graph

    in a loose way • Aligns the build graph with the dependency graph • Moves decision which scope to use to the declaration side • Allows you to add new Dagger modules easily to all apps • Creating new applications is faster
  22. Dagger 2 - Basic @Singleton @Component interface AppComponent class MainApplication

    : Application() { lateinit var component : AppComponent override fun onCreate () { super.onCreate() component = DaggerAppComponent.create() } } :app @Singleton class WeatherRepository @Inject constructor() { suspend fun getForecast(): List<Int> = TODO() } :feature.weather
  23. Anvil - Basic @Singleton @MergeComponent(AppScope::class) interface AppComponent class MainApplication :

    Application() { lateinit var component : AppComponent override fun onCreate () { super.onCreate() component = DaggerAppComponent.create() } } @Singleton class WeatherRepository @Inject constructor() { suspend fun getForecast(): List<Int> = TODO() } interface AppScope :app :feature.weather :lib.scopes
  24. Anvil - Basic @Singleton @MergeComponent(AppScope::class) interface AppComponent Two different names

    to memorize
  25. Anvil - Basic @SingleIn(AppScope::class) @MergeComponent(AppScope::class) interface AppComponent Two different names

    to memorize
  26. Anvil - Basic @Singleton @MergeComponent(AppScope::class) interface AppComponent class MainApplication :

    Application() { lateinit var component: AppComponent override fun onCreate() { super.onCreate() component = DaggerAppComponent.create() } } @Singleton class WeatherRepository @Inject constructor() { suspend fun getForecast(): List<Int> = TODO() } interface AppScope :app :feature.weather :lib.scopes @Scope @Retention(AnnotationRetention.RUNTIME) annotation class SingleIn(val clazz: KClass<*>)
  27. Anvil - Basic @SingleIn(AppScope::class) @MergeComponent(AppScope::class) interface AppComponent class MainApplication :

    Application() { lateinit var component: AppComponent override fun onCreate() { super.onCreate() component = DaggerAppComponent.create() } } @SingleIn(AppScope::class) class WeatherRepository @Inject constructor() { suspend fun getForecast(): List<Int> = TODO() } interface AppScope :app :feature.weather :lib.scopes @Scope @Retention(AnnotationRetention.RUNTIME) annotation class SingleIn(val clazz: KClass<*>)
  28. Dagger 2 - Interface + Impl @Singleton @Component(modules = [WeatherModule::class])

    interface AppComponent interface WeatherRepository { suspend fun getForecast(): List<Int> } :lib.weather @Singleton class WeatherRepositoryImpl @Inject constructor () : WeatherRepository { override suspend fun getForecast (): List<Int> = TODO() } @Module abstract class WeatherModule { @Binds abstract fun bindsWeatherRepository (r: WeatherRepositoryImpl ): WeatherRepository } :app :feature.weather BOILERPLATE 🔥
  29. Anvil - Interface + Impl @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) class WeatherRepositoryImpl @Inject

    constructor() : WeatherRepository { override suspend fun getForecast(): List<Int> = TODO() } :lib.weather :feature.weather interface WeatherRepository { suspend fun getForecast(): List<Int> } @Singleton @MergeComponent(AppScope::class) interface AppComponent :app
  30. Dagger 2 - Binding Interface @Singleton @Component interface AppComponent :

    WeatherBindings interface WeatherBindings { fun inject(fragment: WeatherFragment) fun weatherRepository(): WeatherRepository } :app :feature.weather BOILERPLATE 🔥
  31. Anvil - Binding Interface @ContributesTo(AppScope::class) interface WeatherBindings { fun inject(fragment:

    WeatherFragment) fun weatherRepository(): WeatherRepository } :feature.weather @Singleton @MergeComponent(AppScope::class) interface AppComponent :app
  32. Dagger 2 - Multibindings @Singleton @Component(modules = [WeatherModule::class]) interface AppComponent

    interface WeatherDataSource { suspend fun getForecast (): List<Int> } class WeatherChannelDataSource @Inject constructor () : WeatherDataSource { override suspend fun getForecast (): List<Int> = TODO() } class WeatherUndergroundDataSource @Inject constructor (): WeatherDataSource { override suspend fun getForecast (): List<Int> = TODO() } @Singleton class WeatherRepository @Inject constructor ( private val userPrefs: UserPrefs, private val dataSources : Map<String, WeatherDataSource>, ) { suspend fun getForecast (): List<Int> = TODO() } @Module abstract class WeatherModule { @Binds @IntoMap @StringKey ("WeatherChannel" ) abstract fun bindsWeatherChannel (s: WeatherChannelDataSource ): WeatherDataSource @Binds @IntoMap @StringKey ("WeatherUnderground" ) abstract fun bindsWeatherUnderground (s: WeatherUndergroundDataSource ): WeatherDataSource } :app :feature.weather BOILERPLATE 🔥 BOILERPLATE 🔥
  33. Anvil - Multibindings @Singleton @MergeComponent(AppScope::class) interface AppComponent :app :feature.weather interface

    WeatherDataSource { suspend fun getForecast(): List<Int> } @StringKey("WeatherChannel") @ContributesMultibinding(AppScope::class) class WeatherChannelDataSource @Inject constructor() : WeatherDataSource { override suspend fun getForecast(): List<Int> = TODO() } @StringKey("WeatherUnderground") @ContributesMultibinding(AppScope::class) class WeatherUndergroundDataSource @Inject constructor(): WeatherDataSource { override suspend fun getForecast(): List<Int> = TODO() } @Singleton class WeatherRepository @Inject constructor( private val userPrefs: UserPrefs, private val dataSources: Map<String, WeatherDataSource>, ) { suspend fun getForecast(): List<Int> = TODO() }
  34. /* :app /src/main */ @SingleIn(AppScope::class) @MergeComponent(AppScope::class) interface AppComponent open class

    MainApplication : Application() { lateinit var daggerComponent: Any override fun onCreate() { super.onCreate() daggerComponent = DaggerAppComponent.create() } } Anvil - Replace dependencies in tests
  35. /* :app /src/androidTest */ @SingleIn(AppScope::class) @MergeComponent(AppScope::class) interface TestAppComponent class TestApplication

    : MainApplication() { override lateinit var daggerComponent: Any override fun onCreate() { super.onCreate() daggerComponent = DaggerTestAppComponent.create() } } Anvil - Replace dependencies in tests
  36. /* :lib.weatherdata /src/main */ interface WeatherRepository { suspend fun getForecast():

    List<Int> } /* :features.weatherdata /src/main */ @SingleIn(WeatherScope::class) @ContributesBinding(WeatherScope::class) class WeatherRepositoryImpl @Inject constructor() : WeatherRepository Anvil - Replace dependencies in tests
  37. /* :lib.weatherdata /src/main */ interface WeatherRepository { suspend fun getForecast():

    List<Int> } /* :app /src/androidTest */ @ContributesBinding( scope = WeatherScope::class ) object FakeWeatherRepository : WeatherRepository { override suspend fun getForecast(): List<Int> = listOf(1, 2, 3, 4, 5) } Anvil - Replace dependencies in tests
  38. /* :lib.weatherdata /src/main */ interface WeatherRepository { suspend fun getForecast():

    List<Int> } /* :app /src/androidTest */ @ContributesBinding( scope = WeatherScope::class, replaces = [WeatherRepositoryImpl::class] ) object FakeWeatherRepository : WeatherRepository { override suspend fun getForecast(): List<Int> = listOf(1, 2, 3, 4, 5) } Anvil - Replace dependencies in tests
  39. /* :lib.weatherdata /src/main */ interface WeatherRepository { suspend fun getForecast():

    List<Int> } /* :app /src/androidTest */ @ContributesBinding( scope = WeatherScope::class, replaces = [WeatherRepositoryImpl::class] ) object FakeWeatherRepository : WeatherRepository { override suspend fun getForecast(): List<Int> = listOf(1, 2, 3, 4, 5) } Anvil - Replace dependencies in tests
  40. What about Hilt? • Multi-login • Guarantee no user information

    leaks across accounts • Null safe access to account
  41. What about Hilt? class WorkoutFragment : Fragment( R.layout.workout_fragment), DaggerComponentOwner {

    // github/gpeal/Anvil-Sample override val daggerComponent : WeatherComponent by fragmentComponent { scope, app -> app.bindings<WorkoutComponent .ParentBindings >().workoutComponentBuilder().create(scope) } }
  42. What about Hilt?

  43. What about Hilt?

  44. Anvil - Build Time

  45. Anvil - Dagger Factory Generation plugins { id 'org.jetbrains.kotlin.kapt' id

    'com.squareup.anvil' } dependencies { api "com.google.dagger:dagger:$daggerVersion" kapt "com.google.dagger:dagger-compiler:$daggerVersion" }
  46. Anvil - Dagger Factory Generation plugins { id 'com.squareup.anvil' }

    anvil { generateDaggerFactories = true } dependencies { api "com.google.dagger:dagger:$daggerVersion" }
  47. Build time for single modules improved by up to 65%

    Anvil - Dagger Factory Generation
  48. Build time for our full project improved by 16% on

    average Anvil - Build time
  49. Build time for Slack improved by 25% Anvil - Build

    time
  50. TL;DR • If you use Dagger already, then Anvil will

    make your build times significantly faster and provide more features on top ◦ (For Dagger + Hilt the diff is even bigger) Anvil - Build time
  51. Extend Anvil - Custom Code Generator interface WeatherApi { @GET("…")

    suspend fun getForecast(): Forecast } @Module @ContributesTo(AppScope::class) object WeatherApiModule { @Provides @Reusable fun providesWeatherApi(retrofit: Retrofit): WeatherApi { return retrofit.create() } } @ContributesApi(AppScope::class) Eliminated // github/gpeal/Avil-Sample
  52. @Module @ContributesTo(AppScope::class) public abstract class WeatherViewModelModule { @Binds @IntoMap @ViewModelKey(WeatherViewModel::class)

    public abstract fun bindWeatherViewModelFactory( factory: WeatherViewModel.Factory, ): TonalViewModelFactory<*, *> } Extend Anvil - Custom Code Generator @ContributesViewModel(UserScope::class) class WeatherViewModel @AssistedInject constructor( @Assisted initialState: WeatherState, weatherRepository: WeatherRepository, ) : MavericksViewModel<WeatherState>(initialState) { … } Eliminated
  53. Extend Anvil - Custom Code Generator @ContributesFeatureFlag enum class MyFeatureFlag(

    override val flag: String, ) : BaseFeatureFlag { MyFlag("my-flag"), MyFlag2("my-flag-2"), } @Module @ContributesTo(AppScope::class) public abstract class MyFeatureFlagModule { @Provides @ElementsIntoSet fun providesMyFeatureFlags(): Set<BaseFeatureFlag> = MyFeatureFlag.values().toSet() } Eliminated
  54. Extend Anvil - Custom Code Generator

  55. Thank you! Ralf Wondratschek @vRallev Gabriel Peal @gpeal8 github.com/gpeal/Anvil-Sample

  56. Resources • https://github.com/square/anvil • https://github.com/gpeal/Anvil-Sample • https://gpeal.medium.com/dagger-anvil-learning-to-love-d ependency-injection-on-android-8fad3d5530c9