kotlin-inject-anvil

kotlin-inject is a compile-time dependency injection
framework for Kotlin Multiplatform similar to Dagger 2 for Java.
Anvil extends Dagger 2 to simplify dependency injection.
This project provides a similar feature set for the kotlin-inject framework. The extensions provided
by kotlin-inject-anvil allow you to contribute and automatically merge component interfaces without explicit
references in code.
@ContributesTo(AppScope::class)
interface AppIdComponent {
@Provides
: String =
}
:
component = AppComponent::.create()
From the above example code snippet:
AppIdComponent will be made a super type of the final component and the
provider method is known to the object graph, so you can inject and use AppId anywhere.
- A binding for
RealAuthenticator will be generated and the type Authenticator can safely be injected anywhere.
- Note that neither
AppIdComponent nor RealAuthenticator need to be referenced anywhere else in your code.
Setup
The project comes with a KSP plugin and a runtime module:
dependencies {
kspCommonMainMetadata "software.amazon.lastmile.kotlin.inject.anvil:compiler:$version"
commonMainImplementation "software.amazon.lastmile.kotlin.inject.anvil:runtime:$version"
// Optional module, but strongly suggested to import. It contains the
// @SingleIn scope and @ForScope qualifier annotation together with the
// AppScope::class marker.
commonMainImplementation "software.amazon.lastmile.kotlin.inject.anvil:runtime-optional:$version"
}
You should setup kotlin-inject as described in the official docs.
For details how to setup KSP itself for multiplatform projects, see the
official documentation.
Snapshot builds
To import snapshot builds use following repository:
maven {
url = 'https://central.sonatype.com/repository/maven-snapshots/'
}
Usage
Contributions
@ContributesTo
Component interfaces can be contributed using the @ContributesTo annotation:
@ContributesTo(AppScope::class)
interface AppIdComponent {
@Provides
fun provideAppId(): String = "demo app"
}
The scope AppScope::class tells kotlin-inject-anvil in which component to merge this
interface.
@ContributesBinding
kotlin-inject requires you to write
binding / provider methods in order to provide a
type in the object graph. Imagine this API:
interface Authenticator
class RealAuthenticator : Authenticator
Whenever you inject Authenticator the expectation is to receive an instance of
RealAuthenticator. With vanilla kotlin-inject you can achieve this with a provider
method:
@Inject
@SingleIn(AppScope::class)
class RealAuthenticator : Authenticator
@ContributesTo(AppScope::class)
interface AuthenticatorComponent {
@Provides
fun provideAuthenticator(authenticator: RealAuthenticator): Authenticator = authenticator
}
Note that @ContributesTo is leveraged to automatically add the interface to the final component.
However, this is still too much code and can be simplified further with @ContributesBinding:
@Inject
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
class RealAuthenticator : Authenticator
@ContributesBinding will generate a provider method similar to the one above and automatically
add it to the final component.
Multi-bindings
@ContributesBinding supports Set multi-bindings via its multibinding parameter.
@Inject
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class, multibinding = true)
class LoggingInterceptor : Interceptor
@MergeComponent(AppScope::class)
@SingleIn(AppScope::class)
abstract class AppComponent {
abstract val interceptors: Set<Interceptor>
}
@ContributesSubcomponent
The @ContributesSubcomponent annotation allows you to define a subcomponent in any Gradle module,
but the final @Component will be generated when the parent component is merged.
@ContributesSubcomponent(LoggedInScope::class)
@SingleIn(LoggedInScope::class)
interface RendererComponent {
@ContributesSubcomponent.Factory(AppScope::class)
interface Factory {
fun createRendererComponent(): RendererComponent
}
}
For more details on usage of the annotation and behavior
see the documentation.
Assisted injection
When using the @ContributesBinding annotation in combination with the kotlin-inject @Assisted
annotation, then you can inject the factory lambda with the base type as return type:
Note that the above example binds the factory as a lambda because of how
assisted injection
works with kotlin-inject. If you wish to have a more strongly typed interface bound to create the
dependency, then you can create an explicit Factory interface and bind that in a default
implementation. A common pattern looks like this:
interface AuthenticatorFactory {
fun create(credentials: Credentials): Authenticator
}
@Inject
@ContributesBinding(AppScope::class)
class RealAuthenticatorFactory(
private val realAuthenticatorFactory: (Credentials) -> RealAuthenticator,
) : AuthenticatorFactory {
: Authenticator = realAuthenticatorFactory(credentials)
}
Merging
With kotlin-inject, components are defined similar to the one below in order to instantiate your
object graph at runtime:
@Component
@SingleIn(AppScope::class)
interface AppComponent
In order to pick up all contributions, you must change the @Component annotation to
@MergeComponent:
@MergeComponent(AppScope::class)
@SingleIn(AppScope::class)
interface AppComponent
This will generate a new component class with the original @Component annotation and merge all
contributions to the scope AppScope.
To instantiate the component at runtime, call the generated create() function:
val component = AppComponent::class.create()
Parameters
Parameters are supported the same way as with kotlin-inject:
@MergeComponent(AppScope::class)
@SingleIn(AppScope::class)
abstract class AppComponent(
@get:Provides val userId: String,
)
val component = AppComponent::class.create("userId")
Kotlin Multiplatform
With Kotlin Multiplatform there is a high chance that the generated code cannot be referenced
from common Kotlin code or from common platform code like iosMain. This is due to how
common source folders are separated from platform source folders.
For more details and recommendations setting up kotlin-inject in Kotlin Multiplatform projects
see the official guide.
To address this issue, you can define an expect fun in the common source code next to
component class itself. The actual fun will be generated and create the component. The
function must be annotated with @MergeComponent.CreateComponent. It's optional to have a
receiver type of KClass with your component type as argument. The number of parameters
must match the arguments of your component and the return type must be your component, e.g.
your component in common code could be declared as:
@MergeComponent(AppScope::class)
@SingleIn(AppScope::class)
abstract class AppComponent(
@get:Provides userId: String,
)
: AppComponent
: AppComponent
The generated actual fun will be generated and will look like this:
actual fun create(appId: String): AppComponent {
return KotlinInjectAppComponent::class.create(appId)
}
Scopes
The plugin builds a connection between contributions and merged components through the scope
parameters. Scope classes are only markers and have no further meaning besides building a
connection between contributions and merging them. The class AppScope from the sample could
look like this:
object AppScope
Scope classes are independent of the kotlin-inject
scopes. It's still necessary to set a scope for
the kotlin-inject components or to make instances a singleton in a scope, e.g.
@Inject
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
class RealAuthenticator : Authenticator
@MergeComponent(AppScope::class)
@SingleIn(AppScope::class)
interface AppComponent
kotlin-inject-anvil provides the
@SingleIn scope annotation
optionally by importing following module. We strongly recommend to use the annotation for
consistency.
dependencies {
commonMainImplementation "software.amazon.lastmile.kotlin.inject.anvil:runtime-optional:$version"
}
Sample
A sample project for Android and iOS is available.
Talk
The idea and more background about this library is covered in this
public talk.
Advanced options
Custom symbol processors
kotlin-inject-anvil is extensible and you can create your own annotations and KSP symbol
processors. In the generated code you can reference annotations from kotlin-inject-anvil itself
and build logic on top of them.
For example, assume this is your annotation:
@Target(CLASS)
@ContributingAnnotation
annotation class MyCustomAnnotation
Your custom KSP symbol processor uses this annotation as trigger and generates following code:
@ContributesTo(AppScope::class)
interface MyCustomComponent {
@Provides
fun provideMyCustomType(): MyCustomType = ...
}
This generated component interface MyCustomComponent will be picked up by kotlin-inject-anvil's
symbol processors and contributed to the AppScope due to the @ContributesTo annotation.
Custom annotations and symbol processors are very powerful and allow you to adjust
kotlin-inject-anvil to your needs and your codebase.
There are two ways to indicate these to kotlin-inject-anvil. This is important for incremental
compilation and multi-round support.
Disabling processors
In some occasions the behavior of certain built-in symbol processors of kotlin-inject-anvil
doesn't meet expectations or should be changed. The recommendation in this case is to disable
the built-in processors and create your own. A processor can be disabled through KSP options, e.g.
ksp {
arg("software.amazon.lastmile.kotlin.inject.anvil.processor.ContributesBindingProcessor", "disabled")
}
The key of the option must match the fully qualified name of the symbol processor and the value
must be disabled. All other values will keep the processor enabled. All built-in symbol
processors are part of
this package.
Security
See CONTRIBUTING for more information.
License
This project is licensed under the Apache-2.0 License.