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

Extending kotlin-inject for fun & profit

Ralf
June 06, 2024

Extending kotlin-inject for fun & profit

Kotlin-inject is a dependency injection framework for Kotlin Multiplatform. It verifies the dependency graph during compilaton and generates the necessary code to instantiate the object graph at runtime. It offers a similar feature set as Dagger 2 without the limitation of being tied to the JVM and Android only.

This talk will offer a short introduction to kotlin-inject and discuss its benefits and downsides and how the framework scales in large, modularized code bases. To close some of the gaps and solve project specific use cases KSP and custom code generators will be used to extend the framework and to give us features similar to the ones Anvil provides for Dagger 2. These practical examples serve as an introduction and blueprint into the meta-programming world to reduce boilerplate and simplify patterns in your code base.

Ralf

June 06, 2024
Tweet

More Decks by Ralf

Other Decks in Technology

Transcript

  1. Background • We like Dagger 2 for its many benefits

    • We like Anvil for its simplicity • But both don’t work with KMP in common code
  2. Dagger 2 interface WeatherRepository { fun provideForecast(location: Location): Forecast }

    class WeatherRepositoryImpl( client: HttpClient, ) : WeatherRepository { ... }
  3. Dagger 2 interface WeatherRepository { fun provideForecast(location: Location): Forecast }

    class WeatherRepositoryImpl( client: HttpClient, ) : WeatherRepository { ... } @Module object WeatherRepositoryModule { @Provides @Singleton fun provideWeatherRepository(client: HttpClient): WeatherRepository { return WeatherRepositoryImpl(client) } }
  4. Dagger 2 interface WeatherRepository { fun provideForecast(location: Location): Forecast }

    class WeatherRepositoryImpl( client: HttpClient, ) : WeatherRepository { ... } @Module object WeatherRepositoryModule { @Provides @Singleton fun provideWeatherRepository(client: HttpClient): WeatherRepository { return WeatherRepositoryImpl(client) } }
  5. Dagger 2 interface WeatherRepository { fun provideForecast(location: Location): Forecast }

    class WeatherRepositoryImpl( client: HttpClient, ) : WeatherRepository { ... } @Module object WeatherRepositoryModule { @Provides @Singleton fun provideWeatherRepository(client: HttpClient): WeatherRepository { return WeatherRepositoryImpl(client) } }
  6. Dagger 2 @Singleton @Component( modules = [ WeatherRepositoryModule::class, ] )

    interface AppComponent val appComponent = DaggerAppComponent.create()
  7. Dagger 2 @Singleton @Component( modules = [ WeatherRepositoryModule::class, ] )

    interface AppComponent { fun weatherRepository(): WeatherRepository } val appComponent = DaggerAppComponent.create() val weatherRepository = appComponent.weatherRepository()
  8. Dagger 2 @Singleton @Component( modules = [ WeatherRepositoryModule::class, ] )

    interface AppComponent { fun weatherRepository(): WeatherRepository } val appComponent = DaggerAppComponent.create() val weatherRepository = appComponent.weatherRepository()
  9. Dagger 2 class WeatherRepositoryImpl( client: HttpClient, ) : WeatherRepository {

    ... } @Module object WeatherRepositoryModule { @Provides @Singleton fun provideWeatherRepository(client: HttpClient): WeatherRepository { return WeatherRepositoryImpl(client) } }
  10. Dagger 2 @Singleton class WeatherRepositoryImpl @Inject constructor( client: HttpClient, )

    : WeatherRepository @Module object WeatherRepositoryModule { @Provides @Singleton fun provideWeatherRepository(client: HttpClient): WeatherRepository { return WeatherRepositoryImpl(client) } }
  11. Dagger 2 @Singleton class WeatherRepositoryImpl @Inject constructor( client: HttpClient, )

    : WeatherRepository @Module object WeatherRepositoryModule { @Provides @Singleton fun provideWeatherRepository(client: HttpClient): WeatherRepository { return WeatherRepositoryImpl(client) } }
  12. Dagger 2 @Singleton class WeatherRepositoryImpl @Inject constructor( client: HttpClient, )

    : WeatherRepository @Module interface WeatherRepositoryModule { @Binds fun bindWeatherRepository( weatherRepositoryImpl: WeatherRepositoryImpl ): WeatherRepository }
  13. Dagger 2 @Singleton @Component( modules = [ WeatherRepositoryModule::class, ] )

    interface AppComponent { fun weatherRepository(): WeatherRepository }
  14. Dagger 2 interface WeatherComponent { fun weatherRepository(): WeatherRepository } @Singleton

    @Component( modules = [ WeatherRepositoryModule::class, ] ) interface AppComponent : WeatherComponent
  15. Dagger 2 @Singleton class WeatherRepositoryImpl @Inject constructor( client: HttpClient, )

    : WeatherRepository @Module interface WeatherRepositoryModule { @Binds fun bindWeatherRepository( weatherRepositoryImpl: WeatherRepositoryImpl ): WeatherRepository }
  16. Anvil @Singleton class WeatherRepositoryImpl @Inject constructor( client: HttpClient, ) :

    WeatherRepository @Module @ContributesTo(AppScope::class) interface WeatherRepositoryModule { @Binds fun bindWeatherRepository( weatherRepositoryImpl: WeatherRepositoryImpl ): WeatherRepository }
  17. Anvil @Singleton @ContributesBinding(AppScope::class) class WeatherRepositoryImpl @Inject constructor( client: HttpClient, )

    : WeatherRepository @Module @ContributesTo(AppScope::class) interface WeatherRepositoryModule { @Binds fun bindWeatherRepository( weatherRepositoryImpl: WeatherRepositoryImpl ): WeatherRepository }
  18. Anvil - Other benefits • Removes boilerplate • Improves build

    times significantly • Easy to replace bindings in tests • Extensible with custom code generators
  19. kotlin-inject • Compile-time dependency injection framework for Kotlin • Architecture

    very similar to Dagger 2 • Similar feature set as Dagger 2: Provider methods, components, scopes, qualifiers, multi-bindings, assisted injection, lazy injection, … • Unique features: KMP support, uses KSP, no modules, no binding methods, override provider functions, function support, default arguments, …
  20. Dagger 2 @Singleton class WeatherRepositoryImpl @Inject constructor( client: HttpClient, )

    : WeatherRepository @Module interface WeatherRepositoryModule { @Binds fun bindWeatherRepository( weatherRepositoryImpl: WeatherRepositoryImpl ): WeatherRepository }
  21. kotlin-inject @Singleton @Inject class WeatherRepositoryImpl( client: HttpClient, ) : WeatherRepository

    @Module interface WeatherRepositoryModule { @Binds fun bindWeatherRepository( weatherRepositoryImpl: WeatherRepositoryImpl ): WeatherRepository }
  22. kotlin-inject @Singleton @Inject class WeatherRepositoryImpl( client: HttpClient, ) : WeatherRepository

    @Module interface WeatherRepositoryModule { @Binds fun bindWeatherRepository( weatherRepositoryImpl: WeatherRepositoryImpl ): WeatherRepository }
  23. kotlin-inject @Singleton @Inject class WeatherRepositoryImpl( client: HttpClient, ) : WeatherRepository

    interface WeatherRepositoryComponent { @Provides fun provideWeatherRepository( weatherRepository: WeatherRepositoryImpl ): WeatherRepository = weatherRepository }
  24. Dagger 2 @Singleton @Component( modules = [ WeatherRepositoryModule::class, ] )

    interface AppComponent val appComponent = DaggerAppComponent.create()
  25. kotlin-inject @Singleton @Component interface AppComponent : WeatherRepositoryComponent { val weatherRepository:

    WeatherRepository } val appComponent = AppComponent::class.create() val weatherRepository = appComponent.weatherRepository
  26. kotlin-inject interface WeatherComponent { val weatherRepository: WeatherRepository } @Singleton @Component

    interface AppComponent : WeatherRepositoryComponent, WeatherComponent
  27. kotlin-inject - the good Reduced API surface: Member injection @Binds

    @BindsInstance @Module @Subcomponent @Assisted @AssistedInject
  28. kotlin-inject - the good Arguments: @Component abstract class AppComponent( @get:Provides

    val application: Application, ) val application = … val appComponent = AppComponent::class.create(application)
  29. kotlin-inject - the good Component inheritance: @Component abstract class AppComponent

    @Component abstract class LoggedInComponent( @get:Provides val user: User, )
  30. kotlin-inject - the good Component inheritance: @Component abstract class AppComponent

    @Component abstract class LoggedInComponent( @Component val appComponent: AppComponent, @get:Provides val user: User, )
  31. kotlin-inject - the good Component inheritance: @Component abstract class AppComponent

    @Component abstract class LoggedInComponent( @Component val appComponent: AppComponent, @get:Provides val user: User, ) LoggedInComponent::class.create(appComponent, user)
  32. kotlin-inject - the good Override functions @Component abstract class AppComponent

    @Component abstract class LoggedInComponent(...) { interface Factory { fun loggedInComponent(user: User): LoggedInComponent } }
  33. kotlin-inject - the good Override functions @Component abstract class AppComponent

    : LoggedInComponent.Factory { override fun loggedInComponent(user: User): LoggedInComponent { return LoggedInComponent::class.create(this, user) } } @Component abstract class LoggedInComponent(...) { interface Factory { fun loggedInComponent(user: User): LoggedInComponent } }
  34. kotlin-inject - the good Kotlin support Lazy, default arguments, assisted

    injection, overridden functions, … val viewModels: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>
  35. kotlin-inject - the good Kotlin support Lazy, default arguments, assisted

    injection, overridden functions, … val viewModels: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>
  36. kotlin-inject - the bad Qualifiers typealias AppScopeCoroutineScope = CoroutineScope typealias

    LoggedInScopeCoroutineScope = CoroutineScope typealias PresenterCoroutineScope = CoroutineScope … https://github.com/evant/kotlin-inject/issues/253
  37. kotlin-inject - best practices :app1 AppComponent, LoggedInComponent :weather WeatherRepositoryModule :forecast

    WeatherComponent :app2 AppComponent, LoggedInComponent :app3 AppComponent, LoggedInComponent
  38. kotlin-inject - best practices Naming: interface WeatherRepositoryComponent { val weatherRepository:

    WeatherRepository @Provides fun provideWeatherRepository( weatherRepository: WeatherRepositoryImpl ): WeatherRepository = weatherRepository }
  39. kotlin-inject - best practices Naming: interface ${descriptiveName}Component { val weatherRepository:

    WeatherRepository @Provides fun provide${returnType}( weatherRepository: WeatherRepositoryImpl ): WeatherRepository = weatherRepository }
  40. kotlin-inject - best practices Naming: interface ${descriptiveName}Component { val weatherRepository:

    WeatherRepository @Provides fun provide${returnType}( weatherRepository: WeatherRepositoryImpl ): WeatherRepository = weatherRepository }
  41. kotlin-inject - best practices Component interfaces as inner classes: abstract

    class ViewRenderer<T : BaseModel> : Renderer<T> { fun init(...) { val coroutineScope = CoroutineScope( rootScopeProvider.rootScope.diComponent<Component>().dispatcher + Job(), ) } interface Component { val dispatcher: MainCoroutineDispatcher } }
  42. kotlin-inject - best practices Component interfaces as inner classes: abstract

    class ViewRenderer<T : BaseModel> : Renderer<T> { fun init(...) { val coroutineScope = CoroutineScope( rootScopeProvider.rootScope.diComponent<Component>().dispatcher + Job(), ) } interface Component { val dispatcher: MainCoroutineDispatcher } }
  43. kotlin-inject - best practices Component per platform @Component abstract class

    AndroidAppComponent( @get:Provides val application: Application, ) : LoggedInComponent.Factory { override fun loggedInComponent(user: User): LoggedInComponent { return AndroidLoggedInComponent::class.create(this, user) } }
  44. kotlin-inject - best practices Component per platform @Component abstract class

    AndroidAppComponent( @get:Provides val application: Application, ) : LoggedInComponent.Factory { override fun loggedInComponent(user: User): LoggedInComponent { return AndroidLoggedInComponent::class.create(this, user) } }
  45. kotlin-inject - best practices Component per platform @Component abstract class

    IosAppComponent( @get:Provides val uiApplication: UIApplication, ) : LoggedInComponent.Factory { override fun loggedInComponent(user: User): LoggedInComponent { return IosLoggedInComponent::class.create(this, user) } }
  46. kotlin-inject - best practices Component per platform @Component abstract class

    IosAppComponent( @get:Provides val uiApplication: UIApplication, ) : LoggedInComponent.Factory { override fun loggedInComponent(user: User): LoggedInComponent { return IosLoggedInComponent::class.create(this, user) } }
  47. kotlin-inject - best practices Component per platform @Component abstract class

    DesktopAppComponent : LoggedInComponent.Factory { override fun loggedInComponent(user: User): LoggedInComponent { return DesktopLoggedInComponent::class.create(this, user) } }
  48. kotlin-inject - best practices Component per platform interface LoggedInComponent( val

    appComponent: AppComponent val user: User ) @Component abstract class AndroidLoggedInComponent( @Component override val appComponent: AndroidAppComponent, @get:Provides override val user: User, ) : LoggedInComponent
  49. kotlin-inject - best practices iOS @Component abstract class IosAppComponent( @get:Provides

    val iosNativeImplementations: IosNativeImplementations, ) : IosNativeImplementations.Component
  50. kotlin-inject - best practices iOS interface IosNativeImplementations { val uiApplication:

    UIApplication interface Component { @Provides fun provideUiApplication( iosNativeImplementations: IosNativeImplementations ): UIApplication = iosNativeImplementations.uiApplication } }
  51. kotlin-inject - boilerplate @Component abstract class AppComponent(...) : Component1, Component2,

    Component3, Component4, Component5, Component6, Component7, Component8
  52. kotlin-inject - boilerplate @Component abstract class AppComponent(...) : Component1, Component2,

    Component3, Component4, Component5, Component6, Component7, Component8, Component9
  53. kotlin-inject - boilerplate @Component abstract class AppComponent(...) : Component1, Component2,

    Component3, Component4, Component5, Component6, Component7, Component8, Component9, …
  54. kotlin-inject - boilerplate interface NewComponent { … } @Component abstract

    class AndroidApp1Component : NewComponent @Component abstract class AndroidApp2Component : NewComponent
  55. kotlin-inject - boilerplate interface NewComponent { … } @Component abstract

    class AndroidApp1Component : NewComponent @Component abstract class AndroidApp2Component : NewComponent @Component abstract class DemoAppComponent : NewComponent
  56. kotlin-inject - boilerplate interface NewComponent { … } @Component abstract

    class AndroidApp1Component : NewComponent @Component abstract class AndroidApp2Component : NewComponent @Component abstract class DemoAppComponent : NewComponent @Component abstract class IosAppComponent : NewComponent
  57. kotlin-inject - boilerplate interface NewComponent { … } @Component abstract

    class AndroidApp1Component : NewComponent @Component abstract class AndroidApp2Component : NewComponent @Component abstract class DemoAppComponent : NewComponent @Component abstract class IosAppComponent : NewComponent @Component abstract class DesktopAppComponent : NewComponent …
  58. KSP // compiler/build.gradle plugins { id 'org.jetbrains.kotlin.jvm' } dependencies {

    implementation "com.google.devtools.ksp:symbol-processing-api:$ksp_version" implementation "com.squareup:kotlinpoet:$kotlin_poet_version" implementation "com.squareup:kotlinpoet-ksp:$kotlin_poet_version" testImplementation "dev.zacsweers.kctfork:core:$kotlin_compile_testing_version" testImplementation "dev.zacsweers.kctfork:ksp:$kotlin_compile_testing_version" }
  59. KSP // compiler/src/main/kotlin/…/YourSymbolProcessor.kt private class YourSymbolProcessor( private val environment: SymbolProcessorEnvironment,

    ) : SymbolProcessor { override fun process(resolver: Resolver): List<KSAnnotated> { return emptyList() } }
  60. kotlin-inject - Anvil • Use the scope as connection between

    contribution and merging side • Find all contributions for a scope
  61. kotlin-inject - Anvil interface WeatherComponent { val weatherRepository: WeatherRepository }

    @Singleton @Component interface AppComponent : WeatherComponent
  62. kotlin-inject - Anvil private class MergeComponentProcessor : SymbolProcessor { override

    fun process(resolver: Resolver): List<KSAnnotated> { val declarations = resolver.getDeclarationsFromPackage(…) ... } }
  63. kotlin-inject - Anvil private class MergeComponentProcessor : SymbolProcessor { override

    fun process(resolver: Resolver): List<KSAnnotated> { val declarations = resolver.getDeclarationsFromPackage(…) ... } }
  64. kotlin-inject - Anvil @ContributesTo interface Component1 @ContributesTo interface Component2 @MergeComponent

    interface AppComponent interface AppComponentMerged : Component1, Component2
  65. kotlin-inject - Anvil @ContributesTo interface Component1 @ContributesTo interface Component2 @MergeComponent

    interface AppComponent interface AppComponentMerged : Component1, Component2 @MergeComponent interface AppComponent : AppComponentMerged
  66. kotlin-inject - Anvil @ContributesTo interface Component1 @ContributesTo interface Component2 @MergeComponent

    interface AppComponent interface AppComponentMerged : Component1, Component2 @MergeComponent interface AppComponent : AppComponentMerged Run kotlin-inject processor
  67. @ContributesTo // Given package com.amazon @ContributesTo @Singleton interface WeatherComponent {

    val weatherRepository: WeatherRepository } // Generate package anvil.lookup @Singleton interface ComAmazonWeatherComponent : WeatherComponent
  68. @ContributesTo // Given package com.amazon @ContributesTo @Singleton interface WeatherComponent {

    val weatherRepository: WeatherRepository } // Generate package anvil.lookup @Singleton interface ComAmazonWeatherComponent : WeatherComponent
  69. @ContributesTo // Given package com.amazon @ContributesTo @Singleton interface WeatherComponent {

    val weatherRepository: WeatherRepository } // Generate package anvil.lookup @Singleton interface ComAmazonWeatherComponent : WeatherComponent
  70. @ContributesTo private fun generateComponentInterface(clazz: KSClassDeclaration) { val componentClassName = ClassName(LOOKUP_PACKAGE,

    clazz.safeClassName) val scope = clazz.scope() val fileSpec = FileSpec.builder(componentClassName) .addType( TypeSpec .interfaceBuilder(componentClassName) .addOriginatingKSFile(clazz.requireContainingFile()) .addAnnotation(scope.toAnnotationSpec()) .addSuperinterface(clazz.toClassName()) .build(), ) .build() fileSpec.writeTo(codeGenerator, aggregating = false) }
  71. @ContributesTo private fun generateComponentInterface(clazz: KSClassDeclaration) { val componentClassName = ClassName(LOOKUP_PACKAGE,

    clazz.safeClassName) val scope = clazz.scope() val fileSpec = FileSpec.builder(componentClassName) .addType( TypeSpec .interfaceBuilder(componentClassName) .addOriginatingKSFile(clazz.requireContainingFile()) .addAnnotation(scope.toAnnotationSpec()) .addSuperinterface(clazz.toClassName()) .build(), ) .build() fileSpec.writeTo(codeGenerator, aggregating = false) }
  72. @ContributesTo private fun generateComponentInterface(clazz: KSClassDeclaration) { val componentClassName = ClassName(LOOKUP_PACKAGE,

    clazz.safeClassName) val scope = clazz.scope() val fileSpec = FileSpec.builder(componentClassName) .addType( TypeSpec .interfaceBuilder(componentClassName) .addOriginatingKSFile(clazz.requireContainingFile()) .addAnnotation(scope.toAnnotationSpec()) .addSuperinterface(clazz.toClassName()) .build(), ) .build() fileSpec.writeTo(codeGenerator, aggregating = false) }
  73. @ContributesTo private fun generateComponentInterface(clazz: KSClassDeclaration) { val componentClassName = ClassName(LOOKUP_PACKAGE,

    clazz.safeClassName) val scope = clazz.scope() val fileSpec = FileSpec.builder(componentClassName) .addType( TypeSpec .interfaceBuilder(componentClassName) .addOriginatingKSFile(clazz.requireContainingFile()) .addAnnotation(scope.toAnnotationSpec()) .addSuperinterface(clazz.toClassName()) .build(), ) .build() fileSpec.writeTo(codeGenerator, aggregating = false) }
  74. @ContributesTo private fun generateComponentInterface(clazz: KSClassDeclaration) { val componentClassName = ClassName(LOOKUP_PACKAGE,

    clazz.safeClassName) val scope = clazz.scope() val fileSpec = FileSpec.builder(componentClassName) .addType( TypeSpec .interfaceBuilder(componentClassName) .addOriginatingKSFile(clazz.requireContainingFile()) .addAnnotation(scope.toAnnotationSpec()) .addSuperinterface(clazz.toClassName()) .build(), ) .build() fileSpec.writeTo(codeGenerator, aggregating = false) }
  75. @ContributesTo @Test fun `a component interface is generated in the

    lookup package for a contributed component interface`() { compile( """ package com.amazon.test import com.amazon.ContributesTo import com.amazon.Singleton @ContributesTo @Singleton interface ComponentInterface """, ) { val generatedComponent = componentInterface.generatedComponent assertThat(generatedComponent.packageName).isEqualTo(LOOKUP_PACKAGE) assertThat(generatedComponent.interfaces).containsExactly(componentInterface) assertThat(generatedComponent).isAnnotatedWith(Singleton::class) } } https://github.com/square/anvil/blob/main/compiler-utils/src/testFixtures/java/com/squareup/anvil/compiler/internal/testing/AnvilCompilation.kt
  76. @ContributesTo val generatedComponent = componentInterface.generatedComponent internal val JvmCompilationResult.componentInterface: Class<*> get()

    = classLoader.loadClass("com.amazon.test.ComponentInterface") internal val Class<*>.generatedComponent: Class<*> get() = classLoader.loadClass( "$LOOKUP_PACKAGE." + canonicalName.split(".").joinToString(separator = "") { it.capitalize() }, )
  77. @ContributesTo val generatedComponent = componentInterface.generatedComponent internal val JvmCompilationResult.componentInterface: Class<*> get()

    = classLoader.loadClass("com.amazon.test.ComponentInterface") internal val Class<*>.generatedComponent: Class<*> get() = classLoader.loadClass( "$LOOKUP_PACKAGE." + canonicalName.split(".").joinToString(separator = "") { it.capitalize() }, )
  78. @ContributesTo val generatedComponent = componentInterface.generatedComponent internal val JvmCompilationResult.componentInterface: Class<*> get()

    = classLoader.loadClass("com.amazon.test.ComponentInterface") internal val Class<*>.generatedComponent: Class<*> get() = classLoader.loadClass( "$LOOKUP_PACKAGE." + canonicalName.split(".").joinToString(separator = "") { it.capitalize() }, )
  79. @MergeComponent // Given package com.amazon @Singleton @Component @MergeComponent interface AppComponent

    : AppComponentMerged // Generate package com.amazon @Singleton interface AppComponentMerged : ${contributedSuperTypes}
  80. @MergeComponent // Given package com.amazon @Singleton @Component @MergeComponent interface AppComponent

    : AppComponentMerged // Generate package com.amazon @Singleton interface AppComponentMerged : ${contributedSuperTypes}
  81. @MergeComponent // Given package com.amazon @Singleton @Component @MergeComponent interface AppComponent

    : AppComponentMerged // Generate package com.amazon @Singleton interface AppComponentMerged : ${contributedSuperTypes}
  82. @MergeComponent val className = ClassName( packageName = clazz.packageName.asString(), simpleNames =

    listOf("${clazz.innerClassNames()}Merged"), ) val scope = clazz.scope() val componentInterfaces = resolver.getDeclarationsFromPackage(LOOKUP_PACKAGE) .filterIsInstance<KSClassDeclaration>() .filter { it.scope().isSameAs(scope) } .toList()
  83. @MergeComponent val className = ClassName( packageName = clazz.packageName.asString(), simpleNames =

    listOf("${clazz.innerClassNames()}Merged"), ) val scope = clazz.scope() val componentInterfaces = resolver.getDeclarationsFromPackage(LOOKUP_PACKAGE) .filterIsInstance<KSClassDeclaration>() .filter { it.scope().isSameAs(scope) } .toList()
  84. @MergeComponent val className = ClassName( packageName = clazz.packageName.asString(), simpleNames =

    listOf("${clazz.innerClassNames()}Merged"), ) val scope = clazz.scope() val componentInterfaces = resolver.getDeclarationsFromPackage(LOOKUP_PACKAGE) .filterIsInstance<KSClassDeclaration>() .filter { it.scope().isSameAs(scope) } .toList()
  85. @MergeComponent val fileSpec = FileSpec.builder(className) .addType( TypeSpec .interfaceBuilder(className) .addAnnotation(scope.toAnnotationSpec()) .addSuperinterfaces(componentInterfaces.map

    { it.toClassName() }) .build(), ) .build() fileSpec.writeTo( codeGenerator = codeGenerator, aggregating = true, originatingKSFiles = buildSet { add(clazz.requireContainingFile()) addAll(componentInterfaces.mapNotNull { it.containingFile }) }, )
  86. @MergeComponent val fileSpec = FileSpec.builder(className) .addType( TypeSpec .interfaceBuilder(className) .addAnnotation(scope.toAnnotationSpec()) .addSuperinterfaces(componentInterfaces.map

    { it.toClassName() }) .build(), ) .build() fileSpec.writeTo( codeGenerator = codeGenerator, aggregating = true, originatingKSFiles = buildSet { add(clazz.requireContainingFile()) addAll(componentInterfaces.mapNotNull { it.containingFile }) }, )
  87. @MergeComponent val fileSpec = FileSpec.builder(className) .addType( TypeSpec .interfaceBuilder(className) .addAnnotation(scope.toAnnotationSpec()) .addSuperinterfaces(componentInterfaces.map

    { it.toClassName() }) .build(), ) .build() fileSpec.writeTo( codeGenerator = codeGenerator, aggregating = true, originatingKSFiles = buildSet { add(clazz.requireContainingFile()) addAll(componentInterfaces.mapNotNull { it.containingFile }) }, )
  88. kotlin-inject @Inject @Singleton class WeatherRepositoryImpl( client: HttpClient, ) : WeatherRepository

    interface WeatherRepositoryComponent { @Provides fun provideWeatherRepository( weatherRepository: WeatherRepositoryImpl ): WeatherRepository = weatherRepository }
  89. kotlin-inject - Anvil @ContributesBinding @Inject @Singleton class WeatherRepositoryImpl( client: HttpClient,

    ) : WeatherRepository interface WeatherRepositoryComponent { @Provides fun provideWeatherRepository( weatherRepository: WeatherRepositoryImpl ): WeatherRepository = weatherRepository }
  90. kotlin-inject interface WeatherComponent { val weatherRepository: WeatherRepository } @Singleton @Component

    interface AppComponent : WeatherRepositoryComponent, WeatherComponent
  91. kotlin-inject - Anvil interface WeatherComponent { val weatherRepository: WeatherRepository }

    @Singleton @Component interface AppComponent : WeatherComponent
  92. kotlin-inject - Anvil @ContributesTo @Singleton interface WeatherComponent { val weatherRepository:

    WeatherRepository } @Singleton @Component interface AppComponent
  93. kotlin-inject - Anvil @ContributesTo @Singleton interface WeatherComponent { val weatherRepository:

    WeatherRepository } @Singleton @Component @MergeComponent interface AppComponent : AppComponentMerged
  94. Summary • KSP is great ◦ KMP support, multiple rounds,

    deferred processing, incremental processing, … ◦ Simple introduction to the meta-programming world • kotlin-inject is great and improving • Find creative ways to make tools and libraries work for you