/kotlin-inject-anvil

Extensions for the kotlin-inject dependency injection framework

Primary LanguageKotlinApache License 2.0Apache-2.0

kotlin-inject-anvil

Maven Central CI

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.
@Component
@MergeComponent(AppScope::class)
@SingleIn(AppScope::class)
interface AppComponent : AppComponentMerged

The generated code will ensure that AppIdComponent is a super type of AppComponent and the provider method is known to the object graph. A binding for RealAuthenticator will be generated and the type Authenticator can safely be injected anywhere. Note that neither AppIdComponent nor RealAuthenticator are referenced anywhere else.

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"
}

For details how to setup KSP itself for multiplatform projects see the official documentation. The setup for kotlin-inject is described here.

Snapshot builds

To import snapshot builds use following repository:

maven {
    url 'https://aws.oss.sonatype.org/content/repositories/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

@Component
@MergeComponent(AppScope::class)
@SingleIn(AppScope::class)
abstract class AppComponent {
    // Will be contributed to this set multi-binding.
    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.

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 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.

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) // 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"
}

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)
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.

  1. This is the preferred option: Annotate your annotation with the @ContributingAnnotation marker and run kotlin-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
  2. 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")
    }

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.