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.
// see the section on "Usage > Merging" to understand
// how AppComponentMerged is generated and must be used.
@Component
@MergeComponent(AppScope::class)
@SingleIn(AppScope::class)
interface AppComponent : AppComponentMerged
From the above example code snippet:
AppIdComponent
will be made a super type ofAppComponent
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
@Component
@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 add the @MergeComponent
annotation:
@Component
@MergeComponent(AppScope::class)
@SingleIn(AppScope::class)
interface AppComponent
This will generate a new interface AppComponentMerged
in the same package as AppComponent
.
This generated interface must be added as super type:
@Component
@MergeComponent(AppScope::class)
@SingleIn(AppScope::class)
interface AppComponent : AppComponentMerged
With this setup any contribution is automatically merged. These steps have to be repeated for every component in your project.
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
@Component
@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)
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.