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
fun provideAppId(): String = "demo app"
}
@Inject
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
class RealAuthenticator : Authenticator
// The final kotlin-inject component.
@MergeComponent(AppScope::class)
@SingleIn(AppScope::class)
interface AppComponent
// Instantiate the component at runtime.
val component = AppComponent::class.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 typeAuthenticator
can safely be injected anywhere. - Note that neither
AppIdComponent
norRealAuthenticator
need to be referenced anywhere else in your code.
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.
To import snapshot builds use following repository:
maven {
url 'https://aws.oss.sonatype.org/content/repositories/snapshots/'
}
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.
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.
@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 {
// Will be contributed to this set multi-binding.
abstract val interceptors: Set<Interceptor>
}
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.
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 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")
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,
)
// Create this function next to your component class. The actual function will be generated.
@CreateComponent
expect fun create(appId: String): AppComponent
// Or with receiver type:
@CreateComponent
expect fun KClass<AppComponent>.create(appId: String): AppComponent
The generated actual fun
will be generated and will look like this:
actual fun create(appId: String): AppComponent {
return KotlinInjectAppComponent::class.create(appId)
}
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) // scope for kotlin-inject
@ContributesBinding(AppScope::class)
class RealAuthenticator : Authenticator
@MergeComponent(AppScope::class)
@SingleIn(AppScope::class) // scope for kotlin-inject
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"
}
A sample project for Android and iOS is available.
The idea and more background about this library is covered in this public talk.
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 // see below for details
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.
- This is the preferred option: Annotate your annotation with the
@ContributingAnnotation
marker and runkotlin-inject-anvil
's compiler over the project the annotation is hosted in. Adding the compiler as described in the the setup is important, otherwise the@ContributingAnnotation
has no effect. With this the annotation is understood as a contributing annotation in all downstream usages of this annotation.@ContributingAnnotation // <--- add this! @Target(CLASS) annotation class MyCustomAnnotation
- Alternatively, if you don't control the annotation or otherwise cannot use option 1, you can
specify custom annotations via the
kotlin-inject-anvil-contributing-annotations
KSP option. This option value is a colon-delimited string whose values are the canonical class names of your custom annotations.ksp { arg("kotlin-inject-anvil-contributing-annotations", "com.example.MyCustomAnnotation") }
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.
See CONTRIBUTING for more information.
This project is licensed under the Apache-2.0 License.