Share
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
Transcript
157 slides
-
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 -
Dagger 2 interface WeatherRepository { fun provideForecast(location: Location): Forecast }
class WeatherRepositoryImpl( client: HttpClient, ) : WeatherRepository { ... } -
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) } } -
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) } } -
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) } } -
Dagger 2 @Singleton @Component( modules = [ WeatherRepositoryModule::class, ] )
interface AppComponent -
Dagger 2 @Singleton @Component( modules = [ WeatherRepositoryModule::class, ] )
interface AppComponent -
Dagger 2 @Singleton @Component( modules = [ WeatherRepositoryModule::class, ] )
interface AppComponent -
Dagger 2 @Singleton @Component( modules = [ WeatherRepositoryModule::class, ] )
interface AppComponent val appComponent = DaggerAppComponent.create() -
Dagger 2 @Singleton @Component( modules = [ WeatherRepositoryModule::class, ] )
interface AppComponent { fun weatherRepository(): WeatherRepository } val appComponent = DaggerAppComponent.create() val weatherRepository = appComponent.weatherRepository() -
Dagger 2 @Singleton @Component( modules = [ WeatherRepositoryModule::class, ] )
interface AppComponent { fun weatherRepository(): WeatherRepository } val appComponent = DaggerAppComponent.create() val weatherRepository = appComponent.weatherRepository() -
Dagger 2 class WeatherRepositoryImpl( client: HttpClient, ) : WeatherRepository {
... } @Module object WeatherRepositoryModule { @Provides @Singleton fun provideWeatherRepository(client: HttpClient): WeatherRepository { return WeatherRepositoryImpl(client) } } -
Dagger 2 @Singleton class WeatherRepositoryImpl @Inject constructor( client: HttpClient, )
: WeatherRepository @Module object WeatherRepositoryModule { @Provides @Singleton fun provideWeatherRepository(client: HttpClient): WeatherRepository { return WeatherRepositoryImpl(client) } } -
Dagger 2 @Singleton class WeatherRepositoryImpl @Inject constructor( client: HttpClient, )
: WeatherRepository @Module object WeatherRepositoryModule { @Provides @Singleton fun provideWeatherRepository(client: HttpClient): WeatherRepository { return WeatherRepositoryImpl(client) } } -
Dagger 2 @Singleton class WeatherRepositoryImpl @Inject constructor( client: HttpClient, )
: WeatherRepository @Module interface WeatherRepositoryModule { @Binds fun bindWeatherRepository( weatherRepositoryImpl: WeatherRepositoryImpl ): WeatherRepository } -
Dagger 2 @Singleton @Component( modules = [ WeatherRepositoryModule::class, ] )
interface AppComponent { fun weatherRepository(): WeatherRepository } -
Dagger 2 interface WeatherComponent { fun weatherRepository(): WeatherRepository } @Singleton
@Component( modules = [ WeatherRepositoryModule::class, ] ) interface AppComponent : WeatherComponent -
Dagger 2 @Singleton class WeatherRepositoryImpl @Inject constructor( client: HttpClient, )
: WeatherRepository @Module interface WeatherRepositoryModule { @Binds fun bindWeatherRepository( weatherRepositoryImpl: WeatherRepositoryImpl ): WeatherRepository } -
Anvil @Singleton class WeatherRepositoryImpl @Inject constructor( client: HttpClient, ) :
WeatherRepository @Module @ContributesTo(AppScope::class) interface WeatherRepositoryModule { @Binds fun bindWeatherRepository( weatherRepositoryImpl: WeatherRepositoryImpl ): WeatherRepository } -
Anvil @Singleton @ContributesBinding(AppScope::class) class WeatherRepositoryImpl @Inject constructor( client: HttpClient, )
: WeatherRepository @Module @ContributesTo(AppScope::class) interface WeatherRepositoryModule { @Binds fun bindWeatherRepository( weatherRepositoryImpl: WeatherRepositoryImpl ): WeatherRepository } -
Dagger 2 @Singleton @Component( modules = [ WeatherRepositoryModule::class, ] )
interface AppComponent : WeatherComponent -
Anvil @Singleton @MergeComponent( scope = AppScope::class, modules = [ WeatherRepositoryModule::class,
] ) interface AppComponent : WeatherComponent -
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 -
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, … -
Dagger 2 @Singleton class WeatherRepositoryImpl @Inject constructor( client: HttpClient, )
: WeatherRepository @Module interface WeatherRepositoryModule { @Binds fun bindWeatherRepository( weatherRepositoryImpl: WeatherRepositoryImpl ): WeatherRepository } -
kotlin-inject @Singleton @Inject class WeatherRepositoryImpl( client: HttpClient, ) : WeatherRepository
@Module interface WeatherRepositoryModule { @Binds fun bindWeatherRepository( weatherRepositoryImpl: WeatherRepositoryImpl ): WeatherRepository } -
kotlin-inject @Singleton @Inject class WeatherRepositoryImpl( client: HttpClient, ) : WeatherRepository
@Module interface WeatherRepositoryModule { @Binds fun bindWeatherRepository( weatherRepositoryImpl: WeatherRepositoryImpl ): WeatherRepository } -
kotlin-inject @Singleton @Inject class WeatherRepositoryImpl( client: HttpClient, ) : WeatherRepository
interface WeatherRepositoryComponent { @Provides fun provideWeatherRepository( weatherRepository: WeatherRepositoryImpl ): WeatherRepository = weatherRepository } -
Dagger 2 @Singleton @Component( modules = [ WeatherRepositoryModule::class, ] )
interface AppComponent val appComponent = DaggerAppComponent.create() -
kotlin-inject @Singleton @Component interface AppComponent : WeatherRepositoryComponent val appComponent =
DaggerAppComponent.create() -
Dagger 2 @Singleton @Component interface AppComponent : WeatherRepositoryComponent val appComponent
= DaggerAppComponent.create() -
kotlin-inject @Singleton @Component interface AppComponent : WeatherRepositoryComponent val appComponent =
AppComponent::class.create() -
kotlin-inject @Singleton @Component interface AppComponent : WeatherRepositoryComponent { val weatherRepository:
WeatherRepository } val appComponent = AppComponent::class.create() val weatherRepository = appComponent.weatherRepository -
kotlin-inject interface WeatherComponent { val weatherRepository: WeatherRepository } @Singleton @Component
interface AppComponent : WeatherRepositoryComponent, WeatherComponent -
kotlin-inject - the good Reduced API surface: Member injection @Binds
@BindsInstance @Module @Subcomponent @Assisted @AssistedInject -
kotlin-inject - the good Arguments: @Component abstract class AppComponent val
appComponent = AppComponent::class.create() -
kotlin-inject - the good Arguments: @Component abstract class AppComponent( @get:Provides
val application: Application, ) val application = … val appComponent = AppComponent::class.create(application) -
kotlin-inject - the good Component inheritance: @Component abstract class AppComponent
@Component abstract class LoggedInComponent( @get:Provides val user: User, ) -
kotlin-inject - the good Component inheritance: @Component abstract class AppComponent
@Component abstract class LoggedInComponent( @Component val appComponent: AppComponent, @get:Provides val user: User, ) -
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) -
kotlin-inject - the good Override functions @Component abstract class AppComponent
@Component abstract class LoggedInComponent(...) -
kotlin-inject - the good Override functions @Component abstract class AppComponent
@Component abstract class LoggedInComponent(...) { interface Factory { fun loggedInComponent(user: User): LoggedInComponent } } -
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 } } -
kotlin-inject - the good Kotlin support Lazy, default arguments, assisted
injection, overridden functions, … -
kotlin-inject - the good Scopes object AppScope object LoggedInScope @SingleIn(AppScope::class)
@SingleIn(LoggedInScope::class) -
kotlin-inject - the good Qualifiers typealias AppScopeCoroutineScope = CoroutineScope typealias
LoggedInScopeCoroutineScope = CoroutineScope typealias PresenterCoroutineScope = CoroutineScope … https://github.com/evant/kotlin-inject/issues/253 -
kotlin-inject - the good Qualifiers typealias AppScopeCoroutineScope = CoroutineScope typealias
LoggedInScopeCoroutineScope = CoroutineScope typealias PresenterCoroutineScope = CoroutineScope … https://github.com/evant/kotlin-inject/issues/253 -
kotlin-inject - the bad No wide adoption Slightly slower at
runtime Slow releases -
kotlin-inject - best practices :app1 AppComponent, LoggedInComponent :weather WeatherRepositoryModule :forecast
WeatherComponent :app2 AppComponent, LoggedInComponent -
kotlin-inject - best practices :app1 AppComponent, LoggedInComponent :weather WeatherRepositoryModule :forecast
WeatherComponent :app2 AppComponent, LoggedInComponent :app3 AppComponent, LoggedInComponent -
kotlin-inject - best practices Scopes: @Inject @Singleton class WeatherRepositoryImpl( client:
HttpClient, ) : WeatherRepository -
kotlin-inject - best practices Naming: interface WeatherRepositoryComponent { val weatherRepository:
WeatherRepository @Provides fun provideWeatherRepository( weatherRepository: WeatherRepositoryImpl ): WeatherRepository = weatherRepository } -
kotlin-inject - best practices Naming: interface ${descriptiveName}Component { val weatherRepository:
WeatherRepository @Provides fun provide${returnType}( weatherRepository: WeatherRepositoryImpl ): WeatherRepository = weatherRepository } -
kotlin-inject - best practices Naming: interface ${descriptiveName}Component { val weatherRepository:
WeatherRepository @Provides fun provide${returnType}( weatherRepository: WeatherRepositoryImpl ): WeatherRepository = weatherRepository } -
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 } } -
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 } } -
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) } } -
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) } } -
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) } } -
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) } } -
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) } } -
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 -
kotlin-inject - best practices iOS @Component abstract class IosAppComponent( @get:Provides
val iosNativeImplementations: IosNativeImplementations, ) : IosNativeImplementations.Component -
kotlin-inject - best practices iOS interface IosNativeImplementations { val uiApplication:
UIApplication interface Component { @Provides fun provideUiApplication( iosNativeImplementations: IosNativeImplementations ): UIApplication = iosNativeImplementations.uiApplication } } -
kotlin-inject - boilerplate @Component abstract class AppComponent(...) : Component1, Component2,
Component3, Component4, Component5 -
kotlin-inject - boilerplate @Component abstract class AppComponent(...) : Component1, Component2,
Component3, Component4, Component5, Component6 -
kotlin-inject - boilerplate @Component abstract class AppComponent(...) : Component1, Component2,
Component3, Component4, Component5, Component6, Component7 -
kotlin-inject - boilerplate @Component abstract class AppComponent(...) : Component1, Component2,
Component3, Component4, Component5, Component6, Component7, Component8 -
kotlin-inject - boilerplate @Component abstract class AppComponent(...) : Component1, Component2,
Component3, Component4, Component5, Component6, Component7, Component8, Component9 -
kotlin-inject - boilerplate @Component abstract class AppComponent(...) : Component1, Component2,
Component3, Component4, Component5, Component6, Component7, Component8, Component9, … -
kotlin-inject - boilerplate interface NewComponent { … } @Component abstract
class AndroidApp1Component : NewComponent -
kotlin-inject - boilerplate interface NewComponent { … } @Component abstract
class AndroidApp1Component : NewComponent @Component abstract class AndroidApp2Component : NewComponent -
kotlin-inject - boilerplate interface NewComponent { … } @Component abstract
class AndroidApp1Component : NewComponent @Component abstract class AndroidApp2Component : NewComponent @Component abstract class DemoAppComponent : NewComponent -
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 -
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 … -
kotlin-inject @Inject @Singleton class WeatherRepositoryImpl( client: HttpClient, ) : WeatherRepository
interface WeatherRepositoryComponent { @Provides fun provideWeatherRepository( weatherRepository: WeatherRepositoryImpl ): WeatherRepository = weatherRepository } -
kotlin-inject-anvil @ContributesBinding(AppScope::class) @Inject @Singleton class WeatherRepositoryImpl( client: HttpClient, ) :
WeatherRepository interface WeatherRepositoryComponent { @Provides fun provideWeatherRepository( weatherRepository: WeatherRepositoryImpl ): WeatherRepository = weatherRepository } -
kotlin-inject interface WeatherComponent { val weatherRepository: WeatherRepository } @Singleton @Component
interface AppComponent : WeatherRepositoryComponent, WeatherComponent -
kotlin-inject-anvil interface WeatherComponent { val weatherRepository: WeatherRepository } @Singleton @Component
interface AppComponent : WeatherComponent -
kotlin-inject-anvil @ContributesTo(AppScope::class) interface WeatherComponent { val weatherRepository: WeatherRepository } @Singleton
@Component interface AppComponent -
kotlin-inject-anvil @ContributesTo(AppScope::class) interface WeatherComponent { val weatherRepository: WeatherRepository } @Singleton
@Component @MergeComponent(AppScope::class) interface AppComponent : AppComponentMerged -
:runtime-optional @Scope annotation class SingleIn( val scope: KClass<*>, ) @Qualifier
annotation class ForScope( val scope: KClass<*>, ) abstract class AppScope private constructor() -
: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 -
: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 -
Extending kotlin-inject-anvil class ComposeCounterRenderer : ComposeRenderer<Model>() { @ContributesTo(RendererScope::class) interface Component
{ @Provides @IntoMap fun provideComposeCounterRendererCounterPresenterModel( renderer: () -> ComposeRenderer<Model> ): Pair<KClass<out BaseModel>, () -> Renderer<*>> = Model::class to renderer @Provides @SingleIn(RendererScope::class) fun provideComposeCounterRenderer(): ComposeCounterRenderer = ComposeCounterRenderer() } } -
:runtime // runtime/build.gradle plugins { id 'org.jetbrains.kotlin.multiplatform' } // runtime/src/commonMain/kotlin/…/ContributesRenderer.kt
@Target(CLASS) @ContributingAnnotation annotation class ContributesRenderer -
:runtime // runtime/build.gradle plugins { id 'org.jetbrains.kotlin.multiplatform' } // runtime/src/commonMain/kotlin/…/ContributesRenderer.kt
@Target(CLASS) @ContributingAnnotation annotation class ContributesRenderer -
: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" } -
:compiler // compiler/src/main/kotlin/…/ContributesRendererProcessorProvider.kt @AutoService(SymbolProcessorProvider::class) class ContributesRendererProcessorProvider : SymbolProcessorProvider { override
fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { return ... } } -
:compiler // compiler/src/main/kotlin/…/ContributesRendererSymbolProcessor.kt private class ContributesRendererSymbolProcessor( private val environment: SymbolProcessorEnvironment,
) : SymbolProcessor { override fun process(resolver: Resolver): List<KSAnnotated> { return emptyList() } } -
:compiler // compiler/src/main/kotlin/…/ContributesRendererSymbolProcessor.kt private class ContributesRendererSymbolProcessor( private val environment: SymbolProcessorEnvironment,
) : SymbolProcessor { override fun process(resolver: Resolver): List<KSAnnotated> { resolver .getSymbolsWithAnnotation(ContributesRenderer::class) .filterIsInstance<KSClassDeclaration>() .onEach { checkIsPublic(it) } .forEach { generateComponentInterface(it) } return emptyList() } } -
: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) } -
: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) } -
: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) } -
: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) } -
: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) } -
: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<Model> { 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 -
: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() }, ) -
: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() }, ) -
: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() }, ) -
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