$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

    View Slide

  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 = TODO()
    }
    :feature.weather

    View Slide

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

    View Slide

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

    View Slide

  5. Dagger 2 Refresher - Multibindings
    @Singleton
    @Component
    interface AppComponent
    :app
    interface WeatherDataSource {
    suspend fun getForecast
    (): List
    }
    class WeatherChannelDataSource @Inject constructor
    () : WeatherDataSource {
    override suspend fun getForecast
    (): List = TODO()
    }
    class WeatherUndergroundDataSource @Inject constructor
    (): WeatherDataSource {
    override suspend fun getForecast
    (): List = TODO()
    }
    @Singleton
    class WeatherRepository @Inject constructor
    (
    private val userPrefs: UserPrefs,
    private val dataSources
    : Map,
    ) {
    suspend fun getForecast
    (): List = 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])

    View Slide

  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

    View Slide

  7. Dagger 2
    ● Why it’s not so good
    ○ Boilerplate
    ○ Complicated to learn (names aren’t great)
    ○ Build time (kapt)

    View Slide

  8. Why Anvil?
    ● Opinionated module structure
    ● Hundreds of demo applications
    → Boilerplate code became a problem

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  15. Why Anvil?
    @Component(modules = [WeatherModule::class])
    interface AppComponent
    @Module
    object WeatherModule

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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 = TODO()
    }
    :feature.weather

    View Slide

  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 = TODO()
    }
    interface AppScope
    :app
    :feature.weather
    :lib.scopes

    View Slide

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

    View Slide

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

    View Slide

  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 = TODO()
    }
    interface AppScope
    :app
    :feature.weather
    :lib.scopes
    @Scope
    @Retention(AnnotationRetention.RUNTIME)
    annotation class SingleIn(val clazz: KClass<*>)

    View Slide

  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 = TODO()
    }
    interface AppScope
    :app
    :feature.weather
    :lib.scopes
    @Scope
    @Retention(AnnotationRetention.RUNTIME)
    annotation class SingleIn(val clazz: KClass<*>)

    View Slide

  28. Dagger 2 - Interface + Impl
    @Singleton
    @Component(modules = [WeatherModule::class])
    interface AppComponent
    interface WeatherRepository {
    suspend fun getForecast(): List
    }
    :lib.weather
    @Singleton
    class WeatherRepositoryImpl @Inject constructor () : WeatherRepository {
    override suspend fun getForecast (): List = TODO()
    }
    @Module
    abstract class WeatherModule {
    @Binds
    abstract fun bindsWeatherRepository (r: WeatherRepositoryImpl ): WeatherRepository
    }
    :app
    :feature.weather
    BOILERPLATE 🔥

    View Slide

  29. Anvil - Interface + Impl
    @SingleIn(AppScope::class)
    @ContributesBinding(AppScope::class)
    class WeatherRepositoryImpl @Inject constructor() : WeatherRepository
    {
    override suspend fun getForecast(): List = TODO()
    }
    :lib.weather
    :feature.weather
    interface WeatherRepository {
    suspend fun getForecast(): List
    }
    @Singleton
    @MergeComponent(AppScope::class)
    interface AppComponent
    :app

    View Slide

  30. Dagger 2 - Binding Interface
    @Singleton
    @Component
    interface AppComponent : WeatherBindings
    interface WeatherBindings {
    fun inject(fragment: WeatherFragment)
    fun weatherRepository(): WeatherRepository
    }
    :app
    :feature.weather
    BOILERPLATE 🔥

    View Slide

  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

    View Slide

  32. Dagger 2 - Multibindings
    @Singleton
    @Component(modules = [WeatherModule::class])
    interface AppComponent
    interface WeatherDataSource {
    suspend fun getForecast
    (): List
    }
    class WeatherChannelDataSource @Inject constructor
    () : WeatherDataSource {
    override suspend fun getForecast
    (): List = TODO()
    }
    class WeatherUndergroundDataSource @Inject constructor
    (): WeatherDataSource {
    override suspend fun getForecast
    (): List = TODO()
    }
    @Singleton
    class WeatherRepository @Inject constructor
    (
    private val userPrefs: UserPrefs,
    private val dataSources
    : Map,
    ) {
    suspend fun getForecast
    (): List = 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 🔥

    View Slide

  33. Anvil - Multibindings
    @Singleton
    @MergeComponent(AppScope::class)
    interface AppComponent
    :app
    :feature.weather
    interface WeatherDataSource {
    suspend fun getForecast(): List
    }
    @StringKey("WeatherChannel")
    @ContributesMultibinding(AppScope::class)
    class WeatherChannelDataSource @Inject constructor() :
    WeatherDataSource {
    override suspend fun getForecast(): List =
    TODO()
    }
    @StringKey("WeatherUnderground")
    @ContributesMultibinding(AppScope::class)
    class WeatherUndergroundDataSource @Inject constructor():
    WeatherDataSource {
    override suspend fun getForecast(): List =
    TODO()
    }
    @Singleton
    class WeatherRepository @Inject constructor(
    private val userPrefs: UserPrefs,
    private val dataSources: Map,
    ) {
    suspend fun getForecast(): List =
    TODO()
    }

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  40. What about Hilt?
    ● Multi-login
    ● Guarantee no user information leaks
    across accounts
    ● Null safe access to account

    View Slide

  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().workoutComponentBuilder().create(scope)
    }
    }

    View Slide

  42. What about Hilt?

    View Slide

  43. What about Hilt?

    View Slide

  44. Anvil - Build Time

    View Slide

  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"
    }

    View Slide

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

    View Slide

  47. Build time for single modules improved by up to 65%
    Anvil - Dagger Factory Generation

    View Slide

  48. Build time for our full project improved by 16% on average
    Anvil - Build time

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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(initialState) {

    }
    Eliminated

    View Slide

  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 = MyFeatureFlag.values().toSet()
    }
    Eliminated

    View Slide

  54. Extend Anvil - Custom Code Generator

    View Slide

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

    View Slide

  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

    View Slide