Slide 1

Slide 1 text

Extending kotlin-inject for fun & profit September 20th, 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 ● Aligns build graph with Gradle dependency graph ● 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

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

Dagger 2 @Singleton @Component interface AppComponent : WeatherRepositoryComponent val appComponent = DaggerAppComponent.create()

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 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 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, ) LoggedInComponent::class.create(appComponent, user)

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 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 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

kotlin-inject - the good KSP KSP (kotlinc)

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

kotlin-inject - the good

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

kotlin-inject - the bad No wide adoption Slightly slower at runtime Slow releases

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

kotlin-inject - best practices Naming: interface ${descriptiveName}Component { val weatherRepository: WeatherRepository @Provides fun provide${returnType}( 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 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 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 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 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 IosAppComponent( @get:Provides val uiApplication: UIApplication, ) : LoggedInComponent.Factory { override fun loggedInComponent(user: User): LoggedInComponent { return IosLoggedInComponent::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 DesktopAppComponent : LoggedInComponent.Factory { override fun loggedInComponent(user: User): LoggedInComponent { return DesktopLoggedInComponent::class.create(this, user) } }

Slide 100

Slide 100 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 101

Slide 101 text

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

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

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

Slide 107

Slide 107 text

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

Slide 108

Slide 108 text

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

Slide 109

Slide 109 text

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

Slide 110

Slide 110 text

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

Slide 111

Slide 111 text

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

Slide 112

Slide 112 text

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

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 interface NewComponent { … }

Slide 115

Slide 115 text

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

Slide 116

Slide 116 text

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

Slide 117

Slide 117 text

kotlin-inject - boilerplate interface NewComponent { … } @Component abstract class AndroidApp1Component : NewComponent @Component abstract class AndroidApp2Component : NewComponent @Component abstract class DemoAppComponent : 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 @Component abstract class IosAppComponent : 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 @Component abstract class DesktopAppComponent : NewComponent …

Slide 120

Slide 120 text

kotlin-inject - boilerplate We really miss Anvil …

Slide 121

Slide 121 text

kotlin-inject-anvil https://github.com/amzn/kotlin-inject-anvil

Slide 122

Slide 122 text

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

Slide 123

Slide 123 text

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

Slide 124

Slide 124 text

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

Slide 125

Slide 125 text

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

Slide 126

Slide 126 text

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

Slide 127

Slide 127 text

kotlin-inject-anvil @ContributesTo(AppScope::class) interface WeatherComponent { val weatherRepository: WeatherRepository } @Singleton @Component interface AppComponent

Slide 128

Slide 128 text

kotlin-inject-anvil @ContributesTo(AppScope::class) interface WeatherComponent { val weatherRepository: WeatherRepository } @Singleton @Component @MergeComponent(AppScope::class) interface AppComponent : AppComponentMerged

Slide 129

Slide 129 text

:runtime-optional @Scope annotation class SingleIn( val scope: KClass<*>, ) @Qualifier annotation class ForScope( val scope: KClass<*>, ) abstract class AppScope private constructor()

Slide 130

Slide 130 text

:runtime-optional @Scope annotation class Singleton @Inject @Singleton @ContributesBinding(AppScope::class) class WeatherRepositoryImpl( client: HttpClient, ) : WeatherRepository @ContributesTo(AppScope::class) interface WeatherComponent { val weatherRepository: WeatherRepository } @Component @MergeComponent(AppScope::class) @Singleton interface AppComponent : AppComponentMerged

Slide 131

Slide 131 text

:runtime-optional @Scope annotation class Singleton @Inject @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) class WeatherRepositoryImpl( client: HttpClient, ) : WeatherRepository @ContributesTo(AppScope::class) interface WeatherComponent { val weatherRepository: WeatherRepository } @Component @MergeComponent(AppScope::class) @SingleIn(AppScope::class) interface AppComponent : AppComponentMerged

Slide 132

Slide 132 text

Extending kotlin-inject-anvil class ComposeCounterRenderer : ComposeRenderer()

Slide 133

Slide 133 text

Extending kotlin-inject-anvil class ComposeCounterRenderer : ComposeRenderer() { @ContributesTo(RendererScope::class) interface Component { @Provides @IntoMap fun provideComposeCounterRendererCounterPresenterModel( renderer: () -> ComposeRenderer ): Pair, () -> Renderer<*>> = Model::class to renderer @Provides @SingleIn(RendererScope::class) fun provideComposeCounterRenderer(): ComposeCounterRenderer = ComposeCounterRenderer() } }

Slide 134

Slide 134 text

Extending kotlin-inject-anvil @ContributesRenderer class ComposeCounterRenderer : ComposeRenderer()

Slide 135

Slide 135 text

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

Slide 136

Slide 136 text

KSP include ':runtime' include ':compiler'

Slide 137

Slide 137 text

:runtime // runtime/build.gradle plugins { id 'org.jetbrains.kotlin.multiplatform' }

Slide 138

Slide 138 text

:runtime // runtime/build.gradle plugins { id 'org.jetbrains.kotlin.multiplatform' } // runtime/src/commonMain/kotlin/…/ContributesRenderer.kt @Target(CLASS) @ContributingAnnotation annotation class ContributesRenderer

Slide 139

Slide 139 text

:runtime // runtime/build.gradle plugins { id 'org.jetbrains.kotlin.multiplatform' } // runtime/src/commonMain/kotlin/…/ContributesRenderer.kt @Target(CLASS) @ContributingAnnotation annotation class ContributesRenderer

Slide 140

Slide 140 text

:compiler // compiler/build.gradle plugins { id 'org.jetbrains.kotlin.jvm' id 'com.google.devtools.ksp' } dependencies { ksp "software.amazon.lastmile.kotlin.inject.anvil:compiler:$kia_version" implementation "software.amazon.lastmile.kotlin.inject.anvil:runtime:$kia_version" 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 141

Slide 141 text

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

Slide 142

Slide 142 text

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

Slide 143

Slide 143 text

:compiler // compiler/src/main/kotlin/…/ContributesRendererSymbolProcessor.kt private class ContributesRendererSymbolProcessor( private val environment: SymbolProcessorEnvironment, ) : SymbolProcessor { override fun process(resolver: Resolver): List { resolver .getSymbolsWithAnnotation(ContributesRenderer::class) .filterIsInstance() .onEach { checkIsPublic(it) } .forEach { generateComponentInterface(it) } return emptyList() } }

Slide 144

Slide 144 text

:compiler private fun generateComponentInterface(clazz: KSClassDeclaration) { val componentClassName = ClassName(LOOKUP_PACKAGE, clazz.safeClassName) val allModels = ... val fileSpec = FileSpec.builder(componentClassName) .addType( TypeSpec .interfaceBuilder(componentClassName) .addOriginatingKSFile(clazz.requireContainingFile()) .addAnnotation(rendererScope) .addOriginAnnotation(clazz) .apply { ... } .addFunctions(allModels.map { createModelBindingFunction(clazz, it) }.toList()) .build(), ) .build() fileSpec.writeTo(codeGenerator, aggregating = false) }

Slide 145

Slide 145 text

:compiler private fun generateComponentInterface(clazz: KSClassDeclaration) { val componentClassName = ClassName(LOOKUP_PACKAGE, clazz.safeClassName) val allModels = ... val fileSpec = FileSpec.builder(componentClassName) .addType( TypeSpec .interfaceBuilder(componentClassName) .addOriginatingKSFile(clazz.requireContainingFile()) .addAnnotation(rendererScope) .addOriginAnnotation(clazz) .apply { ... } .addFunctions(allModels.map { createModelBindingFunction(clazz, it) }.toList()) .build(), ) .build() fileSpec.writeTo(codeGenerator, aggregating = false) }

Slide 146

Slide 146 text

:compiler private fun generateComponentInterface(clazz: KSClassDeclaration) { val componentClassName = ClassName(LOOKUP_PACKAGE, clazz.safeClassName) val allModels = ... val fileSpec = FileSpec.builder(componentClassName) .addType( TypeSpec .interfaceBuilder(componentClassName) .addOriginatingKSFile(clazz.requireContainingFile()) .addAnnotation(rendererScope) .addOriginAnnotation(clazz) .apply { ... } .addFunctions(allModels.map { createModelBindingFunction(clazz, it) }.toList()) .build(), ) .build() fileSpec.writeTo(codeGenerator, aggregating = false) }

Slide 147

Slide 147 text

:compiler private fun generateComponentInterface(clazz: KSClassDeclaration) { val componentClassName = ClassName(LOOKUP_PACKAGE, clazz.safeClassName) val allModels = ... val fileSpec = FileSpec.builder(componentClassName) .addType( TypeSpec .interfaceBuilder(componentClassName) .addOriginatingKSFile(clazz.requireContainingFile()) .addAnnotation(rendererScope) .addOriginAnnotation(clazz) .apply { ... } .addFunctions(allModels.map { createModelBindingFunction(clazz, it) }.toList()) .build(), ) .build() fileSpec.writeTo(codeGenerator, aggregating = false) }

Slide 148

Slide 148 text

:compiler private fun generateComponentInterface(clazz: KSClassDeclaration) { val componentClassName = ClassName(LOOKUP_PACKAGE, clazz.safeClassName) val allModels = ... val fileSpec = FileSpec.builder(componentClassName) .addType( TypeSpec .interfaceBuilder(componentClassName) .addOriginatingKSFile(clazz.requireContainingFile()) .addAnnotation(rendererScope) .addOriginAnnotation(clazz) .apply { ... } .addFunctions(allModels.map { createModelBindingFunction(clazz, it) }.toList()) .build(), ) .build() fileSpec.writeTo(codeGenerator, aggregating = false) }

Slide 149

Slide 149 text

:compiler @Test fun `a component interface is generated in the lookup package for a contributed renderer`() { compile( """ package software.amazon.test import … class Model : BaseModel @ContributesRenderer class TestRenderer : Renderer { override fun render(model: Model) = Unit } """, componentInterfaceSource, ) { val generatedComponent = testRenderer.generatedComponent assertThat(generatedComponent.packageName).isEqualTo(LOOKUP_PACKAGE) assertThat(generatedComponent).isAnnotatedWith(SingleInRendererScope::class) assertThat(generatedComponent.origin).isEqualTo(testRenderer) … } } https://github.com/square/anvil/blob/main/compiler-utils/src/testFixtures/java/com/squareup/anvil/compiler/internal/testing/AnvilCompilation.kt

Slide 150

Slide 150 text

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

Slide 151

Slide 151 text

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

Slide 152

Slide 152 text

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

Slide 153

Slide 153 text

Extending kotlin-inject-anvil @ContributesRenderer class ComposeCounterRenderer : ComposeRenderer()

Slide 154

Slide 154 text

Adoption https://x.com/chrisbanes/status/1833549092770718014

Slide 155

Slide 155 text

Adoption https://github.com/slackhq/circuit/releases/tag/0.24.0

Slide 156

Slide 156 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 ● kotlin-inject-anvil removes boilerplate and is extensible ● Find creative ways to make tools and libraries work for you

Slide 157

Slide 157 text

Extending kotlin-inject for fun & profit Ralf Wondratschek