Slide 1

Slide 1 text

Extending kotlin-inject for fun & profit June 6th, 2024 Ralf Wondratschek

Slide 2

Slide 2 text

https://www.youtube.com/watch?v=xESX82kwjn4

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

https://www.youtube.com/watch?v=0T_zvUEqsD4

Slide 5

Slide 5 text

Background https://ralf-wondratschek.com/presentations

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

Dagger 2 Provider side Component Injection side

Slide 8

Slide 8 text

interface WeatherRepository { fun provideForecast(location: Location): Forecast } Dagger 2

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

Dagger 2 @Singleton @Component( modules = [ WeatherRepositoryModule::class, ] ) interface AppComponent

Slide 14

Slide 14 text

Dagger 2 @Singleton @Component( modules = [ WeatherRepositoryModule::class, ] ) interface AppComponent

Slide 15

Slide 15 text

@Component( modules = [ WeatherRepositoryModule::class, ] ) interface AppComponent Dagger 2

Slide 16

Slide 16 text

Dagger 2 @Singleton @Component( modules = [ WeatherRepositoryModule::class, ] ) interface AppComponent

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

Dagger 2 :app AppComponent :weather WeatherRepositoryModule

Slide 25

Slide 25 text

Dagger 2 :app AppComponent :weather WeatherRepositoryModule :forecast appComponent.weatherRepository()

Slide 26

Slide 26 text

Dagger 2 :app AppComponent :weather WeatherRepositoryModule :forecast appComponent.weatherRepository()

Slide 27

Slide 27 text

Dagger 2 @Singleton @Component( modules = [ WeatherRepositoryModule::class, ] ) interface AppComponent { fun weatherRepository(): WeatherRepository }

Slide 28

Slide 28 text

Dagger 2 interface WeatherComponent { fun weatherRepository(): WeatherRepository } @Singleton @Component( modules = [ WeatherRepositoryModule::class, ] ) interface AppComponent : WeatherComponent

Slide 29

Slide 29 text

Dagger 2 :app AppComponent :weather WeatherRepositoryModule :forecast appComponent.weatherRepository()

Slide 30

Slide 30 text

Dagger 2 :app AppComponent :weather WeatherRepositoryModule :forecast WeatherComponent

Slide 31

Slide 31 text

Anvil

Slide 32

Slide 32 text

Anvil https://ralf-wondratschek.com/presentations

Slide 33

Slide 33 text

Anvil object AppScope @Contributes*(AppScope::class) @MergeComponent(AppScope::class)

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

Anvil @Singleton @ContributesBinding(AppScope::class) class WeatherRepositoryImpl @Inject constructor( client: HttpClient, ) : WeatherRepository

Slide 38

Slide 38 text

Dagger 2 interface WeatherComponent { fun weatherRepository(): WeatherRepository }

Slide 39

Slide 39 text

Anvil @ContributesTo(AppScope::class) interface WeatherComponent { fun weatherRepository(): WeatherRepository }

Slide 40

Slide 40 text

Dagger 2 @Singleton @Component( modules = [ WeatherRepositoryModule::class, ] ) interface AppComponent : WeatherComponent

Slide 41

Slide 41 text

Anvil @Singleton @MergeComponent( scope = AppScope::class, modules = [ WeatherRepositoryModule::class, ] ) interface AppComponent : WeatherComponent

Slide 42

Slide 42 text

Anvil @Singleton @MergeComponent( scope = AppScope::class, ) interface AppComponent : WeatherComponent

Slide 43

Slide 43 text

Anvil @Singleton @MergeComponent(AppScope::class) interface AppComponent : WeatherComponent

Slide 44

Slide 44 text

Anvil @Singleton @MergeComponent(AppScope::class) interface AppComponent

Slide 45

Slide 45 text

Anvil - Other benefits ● Removes boilerplate ● Improves build times significantly ● Easy to replace bindings in tests ● Extensible with custom code generators

Slide 46

Slide 46 text

kotlin-inject

Slide 47

Slide 47 text

“The Dagger 2 for Kotlin Multiplatform.”

Slide 48

Slide 48 text

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, …

Slide 49

Slide 49 text

kotlin-inject import me.tatarka.inject.annotations.* @Scope annotation class Singleton

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

kotlin-inject @Provides fun provideWeatherRepository( weatherRepository: WeatherRepositoryImpl ): WeatherRepository = weatherRepository val WeatherRepositoryImpl.bind: WeatherRepository @Provides get() = this

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

kotlin-inject @Singleton @Component interface AppComponent : WeatherRepositoryComponent val appComponent = AppComponent::class.create()

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

kotlin-inject @Singleton @Component interface AppComponent : WeatherRepositoryComponent { val weatherRepository: WeatherRepository }

Slide 61

Slide 61 text

kotlin-inject interface WeatherComponent { val weatherRepository: WeatherRepository } @Singleton @Component interface AppComponent : WeatherRepositoryComponent, WeatherComponent

Slide 62

Slide 62 text

kotlin-inject :app AppComponent :weather WeatherRepositoryComponent :forecast WeatherComponent

Slide 63

Slide 63 text

kotlin-inject - the good Reduced API surface: Member injection @Binds @BindsInstance @Module @Subcomponent @Assisted @AssistedInject

Slide 64

Slide 64 text

kotlin-inject - the good Arguments: @Component abstract class AppComponent val appComponent = AppComponent::class.create()

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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)

Slide 69

Slide 69 text

kotlin-inject - the good Override functions @Component abstract class AppComponent @Component abstract class LoggedInComponent(...)

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

kotlin-inject - the good Kotlin support Lazy, default arguments, assisted injection, overridden functions, …

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

kotlin-inject - the good KAPT Stub generation (kotlinc)

Slide 76

Slide 76 text

kotlin-inject - the good KAPT Stub generation (kotlinc) KAPT

Slide 77

Slide 77 text

kotlin-inject - the good KAPT Stub generation (kotlinc) KAPT kotlinc

Slide 78

Slide 78 text

kotlin-inject - the good KAPT Stub generation (kotlinc) KAPT kotlinc javac

Slide 79

Slide 79 text

kotlin-inject - the good KSP KSP (kotlinc)

Slide 80

Slide 80 text

kotlin-inject - the good KSP KSP (kotlinc) kotlinc

Slide 81

Slide 81 text

kotlin-inject - the bad

Slide 82

Slide 82 text

kotlin-inject - the bad Scopes object AppScope object LoggedInScope @SingleIn(AppScope::class) @SingleIn(LoggedInScope::class)

Slide 83

Slide 83 text

kotlin-inject - the bad Scopes @SingleInAppScope @SingleInLoggedInScope … https://github.com/evant/kotlin-inject/issues/377

Slide 84

Slide 84 text

kotlin-inject - the bad Qualifiers @ForScope(AppScope::class) @ForScope(LoggedInScope::class)

Slide 85

Slide 85 text

kotlin-inject - the bad Qualifiers typealias AppScopeCoroutineScope = CoroutineScope typealias LoggedInScopeCoroutineScope = CoroutineScope typealias PresenterCoroutineScope = CoroutineScope … https://github.com/evant/kotlin-inject/issues/253

Slide 86

Slide 86 text

kotlin-inject - the bad No wide adoption

Slide 87

Slide 87 text

kotlin-inject - best practices :app1 AppComponent, LoggedInComponent :weather WeatherRepositoryModule :forecast WeatherComponent

Slide 88

Slide 88 text

kotlin-inject - best practices :app1 AppComponent, LoggedInComponent :weather WeatherRepositoryModule :forecast WeatherComponent :app2 AppComponent, LoggedInComponent

Slide 89

Slide 89 text

kotlin-inject - best practices :app1 AppComponent, LoggedInComponent :weather WeatherRepositoryModule :forecast WeatherComponent :app2 AppComponent, LoggedInComponent :app3 AppComponent, LoggedInComponent

Slide 90

Slide 90 text

kotlin-inject - best practices Scopes: @Inject @Singleton class WeatherRepositoryImpl( client: HttpClient, ) : WeatherRepository

Slide 91

Slide 91 text

kotlin-inject - best practices Naming: interface WeatherRepositoryComponent { val weatherRepository: WeatherRepository @Provides fun provideWeatherRepository( weatherRepository: WeatherRepositoryImpl ): WeatherRepository = weatherRepository }

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

kotlin-inject - best practices iOS @Component abstract class IosAppComponent( @get:Provides val iosNativeImplementations: IosNativeImplementations, ) : IosNativeImplementations.Component

Slide 103

Slide 103 text

kotlin-inject - best practices iOS interface IosNativeImplementations { val uiApplication: UIApplication interface Component { @Provides fun provideUiApplication( iosNativeImplementations: IosNativeImplementations ): UIApplication = iosNativeImplementations.uiApplication } }

Slide 104

Slide 104 text

kotlin-inject - boilerplate @Component abstract class AppComponent(...)

Slide 105

Slide 105 text

kotlin-inject - boilerplate @Component abstract class AppComponent(...) : Component1

Slide 106

Slide 106 text

kotlin-inject - boilerplate @Component abstract class AppComponent(...) : Component1, Component2

Slide 107

Slide 107 text

kotlin-inject - boilerplate @Component abstract class AppComponent(...) : Component1, Component2, Component3

Slide 108

Slide 108 text

kotlin-inject - boilerplate @Component abstract class AppComponent(...) : Component1, Component2, Component3, Component4

Slide 109

Slide 109 text

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

Slide 110

Slide 110 text

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

Slide 111

Slide 111 text

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

Slide 112

Slide 112 text

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

Slide 113

Slide 113 text

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

Slide 114

Slide 114 text

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

Slide 115

Slide 115 text

kotlin-inject - boilerplate interface NewComponent { … }

Slide 116

Slide 116 text

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

Slide 117

Slide 117 text

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

Slide 118

Slide 118 text

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

Slide 119

Slide 119 text

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

Slide 120

Slide 120 text

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 …

Slide 121

Slide 121 text

kotlin-inject - boilerplate @Provides fun provideWeatherRepository( weatherRepository: WeatherRepositoryImpl ): WeatherRepository = weatherRepository

Slide 122

Slide 122 text

kotlin-inject - boilerplate We really miss Anvil …

Slide 123

Slide 123 text

KSP ● Supports KMP ● Supports multiple rounds https://kotlinlang.org/docs/ksp-overview.html

Slide 124

Slide 124 text

KSP include ':runtime' include ':compiler'

Slide 125

Slide 125 text

KSP // runtime/build.gradle plugins { id 'org.jetbrains.kotlin.multiplatform' } // runtime/src/commonMain/kotlin/…/YourAnnotation.kt annotation class YourAnnotation

Slide 126

Slide 126 text

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

Slide 127

Slide 127 text

KSP // compiler/src/main/kotlin/…/YourSymbolProcessorProvider.kt @AutoService(SymbolProcessorProvider::class) class YourSymbolProcessorProvider : SymbolProcessorProvider { override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { return ... } }

Slide 128

Slide 128 text

KSP // compiler/src/main/kotlin/…/YourSymbolProcessor.kt private class YourSymbolProcessor( private val environment: SymbolProcessorEnvironment, ) : SymbolProcessor { override fun process(resolver: Resolver): List { return emptyList() } }

Slide 129

Slide 129 text

KSP environment.logger.error(message, symbol)

Slide 130

Slide 130 text

KSP environment.logger.error(message, symbol) > SourceFile.kt:8: Contributed component interfaces must be public.

Slide 131

Slide 131 text

kotlin-inject - Anvil @Target(CLASS) annotation class ContributesTo @Target(CLASS) annotation class MergeComponent

Slide 132

Slide 132 text

kotlin-inject - Anvil ● Use the scope as connection between contribution and merging side ● Find all contributions for a scope

Slide 133

Slide 133 text

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

Slide 134

Slide 134 text

kotlin-inject - Anvil interface WeatherComponent { val weatherRepository: WeatherRepository }

Slide 135

Slide 135 text

kotlin-inject - Anvil @ContributesTo interface WeatherComponent { val weatherRepository: WeatherRepository }

Slide 136

Slide 136 text

kotlin-inject - Anvil @ContributesTo @Singleton interface WeatherComponent { val weatherRepository: WeatherRepository }

Slide 137

Slide 137 text

kotlin-inject - Anvil @Singleton @Component interface AppComponent

Slide 138

Slide 138 text

kotlin-inject - Anvil @Singleton @Component @MergeComponent interface AppComponent

Slide 139

Slide 139 text

kotlin-inject - Anvil @Singleton @Component @MergeComponent interface AppComponent : AppComponentMerged

Slide 140

Slide 140 text

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

Slide 141

Slide 141 text

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

Slide 142

Slide 142 text

kotlin-inject - Anvil @ContributesTo interface Component1

Slide 143

Slide 143 text

kotlin-inject - Anvil @ContributesTo interface Component1 @ContributesTo interface Component2

Slide 144

Slide 144 text

kotlin-inject - Anvil @ContributesTo interface Component1 @ContributesTo interface Component2 @MergeComponent interface AppComponent

Slide 145

Slide 145 text

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

Slide 146

Slide 146 text

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

Slide 147

Slide 147 text

kotlin-inject - Anvil @ContributesTo interface Component1 @ContributesTo interface Component2 @MergeComponent interface AppComponent interface AppComponentMerged : Component1, Component2 @MergeComponent interface AppComponent : AppComponentMerged Run kotlin-inject processor

Slide 148

Slide 148 text

@ContributesTo // Given package com.amazon @ContributesTo @Singleton interface WeatherComponent { val weatherRepository: WeatherRepository }

Slide 149

Slide 149 text

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

Slide 150

Slide 150 text

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

Slide 151

Slide 151 text

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

Slide 152

Slide 152 text

@ContributesTo resolver .getNewSymbolsWithAnnotation(ContributesTo::class) .filterIsInstance() .onEach { checkIsInterface(it) checkIsPublic(it) } .forEach { generateComponentInterface(it) }

Slide 153

Slide 153 text

@ContributesTo fun Resolver.getNewSymbolsWithAnnotation(annotation: KClass<*>): Sequence { val newFiles = getNewFiles().toSet() return getSymbolsWithAnnotation(annotation) .filter { it.containingFile in newFiles } }

Slide 154

Slide 154 text

@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) }

Slide 155

Slide 155 text

@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) }

Slide 156

Slide 156 text

@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) }

Slide 157

Slide 157 text

@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) }

Slide 158

Slide 158 text

@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) }

Slide 159

Slide 159 text

@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

Slide 160

Slide 160 text

@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() }, )

Slide 161

Slide 161 text

@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() }, )

Slide 162

Slide 162 text

@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() }, )

Slide 163

Slide 163 text

@MergeComponent // Given package com.amazon @Singleton @Component @MergeComponent interface AppComponent : AppComponentMerged

Slide 164

Slide 164 text

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

Slide 165

Slide 165 text

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

Slide 166

Slide 166 text

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

Slide 167

Slide 167 text

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

Slide 168

Slide 168 text

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

Slide 169

Slide 169 text

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

Slide 170

Slide 170 text

@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 }) }, )

Slide 171

Slide 171 text

@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 }) }, )

Slide 172

Slide 172 text

@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 }) }, )

Slide 173

Slide 173 text

kotlin-inject - Anvil @ContributesBinding @ContributesSubcomponent

Slide 174

Slide 174 text

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

Slide 175

Slide 175 text

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

Slide 176

Slide 176 text

kotlin-inject - Anvil @ContributesBinding @Inject @Singleton class WeatherRepositoryImpl( client: HttpClient, ) : WeatherRepository

Slide 177

Slide 177 text

kotlin-inject interface WeatherComponent { val weatherRepository: WeatherRepository } @Singleton @Component interface AppComponent : WeatherRepositoryComponent, WeatherComponent

Slide 178

Slide 178 text

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

Slide 179

Slide 179 text

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

Slide 180

Slide 180 text

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

Slide 181

Slide 181 text

kotlin-inject https://github.com/evant/kotlin-inject/discussions/221

Slide 182

Slide 182 text

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

Slide 183

Slide 183 text

Extending kotlin-inject for fun & profit Ralf Wondratschek