Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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 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
  2. 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
  3. Dagger 2 Refresher - Binding Interface @Singleton @Component interface AppComponent

    : WeatherBindings interface WeatherBindings { fun inject(fragment: WeatherFragment) fun weatherRepository(): WeatherRepository } :app :feature.weather
  4. 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])
  5. 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
  6. Dagger 2 • Why it’s not so good ◦ Boilerplate

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

    applications → Boilerplate code became a problem
  8. 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
  9. 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
  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? • 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
  12. 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
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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<*>)
  19. 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<*>)
  20. 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 🔥
  21. 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
  22. Dagger 2 - Binding Interface @Singleton @Component interface AppComponent :

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

    WeatherFragment) fun weatherRepository(): WeatherRepository } :feature.weather @Singleton @MergeComponent(AppScope::class) interface AppComponent :app
  24. 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 🔥
  25. 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() }
  26. /* :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
  27. /* :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
  28. /* :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
  29. /* :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
  30. /* :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
  31. /* :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
  32. What about Hilt? • Multi-login • Guarantee no user information

    leaks across accounts • Null safe access to account
  33. 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) } }
  34. 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" }
  35. Anvil - Dagger Factory Generation plugins { id 'com.squareup.anvil' }

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

    Anvil - Dagger Factory Generation
  37. 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
  38. 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
  39. @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
  40. 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