kotlin-inject-anvil

repository·main·Indexed 18 days ago

https://github.com/vrallev/kotlin-inject-anvil

A dependency injection extension for the kotlin-inject framework designed for Kotlin Multiplatform. It provides an 'Anvil-like' experience by allowing developers to automatically contribute components and bindings to specific scopes using annotations like @ContributesTo, @ContributesBinding, and @MergeComponent, eliminating the need for manual wiring in a central component.

Tokens
3.7K
Snippets
15
Records
16
Agent score
63%

What's inside kotlin-inject-anvil

  1. How kotlin-inject-anvil works

    main

    The project extends kotlin-inject by allowing you to contribute component interfaces and bindings from anywhere in your codebase without needing to explicitly reference them in a central component.

    It uses Scopes (marker classes) to connect contributions to a specific merged component. When you use @MergeComponent(Scope::class), the plugin automatically gathers all interfaces marked with @ContributesTo(Scope::class) and all bindings marked with @ContributesBinding(Scope::class) and merges them into the final component.

    @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()
  2. Understand Scopes in kotlin-inject-anvil

    main

    Scopes in kotlin-inject-anvil are marker classes used to connect contributions to merged components. They do not have inherent logic.

    Important: These scope markers are independent of kotlin-inject's internal scoping mechanism. You must still use kotlin-inject scoping (like @SingleIn) to manage object lifecycles.

    It is highly recommended to use the @SingleIn annotation provided by the runtime-optional module for consistency.

    object AppScope // Marker class
    
    @Inject
    @SingleIn(AppScope::class) // kotlin-inject scope
    @ContributesBinding(AppScope::class) // anvil connection
    class RealAuthenticator : Authenticator
  3. Create custom contributing annotations

    main

    You can extend kotlin-inject-anvil by creating your own annotations and KSP symbol processors. To make your custom annotation work with kotlin-inject-anvil, you must signal that it is a 'contributing annotation' so that components annotated with it (or generated by processors triggered by it) are correctly picked up by the library's symbol processors.

    There are two ways to register custom annotations:

    1. Using the @ContributingAnnotation marker (Preferred): Annotate your custom annotation with @ContributingAnnotation. For this to work, you must have the kotlin-inject-anvil compiler running over the project where the annotation is hosted.
    2. Using KSP options: If you cannot use the marker (e.g., you don't control the annotation), provide the canonical class names of your annotations via the kotlin-inject-anvil-contributing-annotations KSP option as a colon-delimited string.
    // Option 1: Preferred method
    @ContributingAnnotation
    @Target(CLASS)
    annotation class MyCustomAnnotation
    
    // Option 2: KSP configuration if you can't use the marker
    ksp {
      arg("kotlin-inject-anvil-contributing-annotations", "com.example.MyCustomAnnotation")
    }
  4. Use assisted injection with @ContributesBinding

    main

    When using @ContributesBinding with kotlin-inject's @Assisted annotation, the factory is injected as a lambda where the base type is the return type.

    Note: If you require a strongly typed interface instead of a lambda, you should create an explicit Factory interface and bind that manually.

    interface Authenticator {
        fun authenticate(): Result
    }
    
    @Inject
    @ContributesBinding(AppScope::class)
    class RealAuthenticator(
        @Assisted val credentials: Credentials,
    ): Authenticator {
        override fun authenticate(): Result = sendAuthenticationRequest(credentials)
    }
    
    @Inject
    class LoginScreen(val authenticatorFactory: (Credentials) -> Authenticator) {
        fun login(credentials: Credentials) {
            val authenticator = authenticatorFactory(credentials)
            authenticator.authenticate()
        }
    }
  5. Install kotlin-inject-anvil

    main

    To use kotlin-inject-anvil, you need to add the KSP compiler and the runtime module to your dependencies. It is strongly recommended to also include the runtime-optional module to access the @SingleIn scope annotation and @ForScope qualifier.

    Ensure you have already set up kotlin-inject and KSP for your Kotlin Multiplatform project as per their official documentation.

    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"
    }
  6. Handle Kotlin Multiplatform component creation

    main

    In Kotlin Multiplatform, generated code might not be accessible from common or platform-specific source sets (like iosMain). To solve this, use the @CreateComponent annotation on an expect fun in your common code. The KSP plugin will generate the actual fun implementation.

    @MergeComponent(AppScope::class)
    @SingleIn(AppScope::class)
    abstract class AppComponent(
        @get:Provides userId: String,
    )
    
    // The actual implementation will be generated by KSP
    @CreateComponent
    expect fun create(appId: String): AppComponent
    
    // Or using a receiver type
    @CreateComponent
    expect fun KClass<AppComponent>.create(appId: String): AppComponent
  7. Disable built-in symbol processors

    main

    If a built-in kotlin-inject-anvil symbol processor does not meet your requirements, you can disable it using KSP options. To disable a processor, set the KSP argument key to the fully qualified name of the symbol processor and set its value to "disabled". Any other value will leave the processor enabled.

    All built-in symbol processors are located in the software.amazon.lastmile.kotlin.inject.anvil.processor package.

    ksp {
        arg("software.amazon.lastmile.kotlin.inject.anvil.processor.ContributesBindingProcessor", "disabled")
    }
  8. Merge components with @MergeComponent

    main

    To pick up all contributions for a specific scope, replace the standard @Component annotation with @MergeComponent(Scope::class). This generates a new component class that merges all contributions to that scope.

    Instantiation

    Use the generated .create() function to instantiate the component at runtime. If your component has parameters, pass them to .create().

    // Standard component with parameters
    @MergeComponent(AppScope::class)
    @SingleIn(AppScope::class)
    abstract class AppComponent(
        @get:Provides val userId: String,
    )
    
    // Instantiate with parameter
    val component = AppComponent::class.create("userId")
  9. Contribute component interfaces with @ContributesTo

    main

    Use the @ContributesTo annotation to add a component interface to a specific scope. This makes the provider methods within that interface available to the final merged component of that scope.

    @ContributesTo(AppScope::class)
    interface AppIdComponent {
        @Provides
        fun provideAppId(): String = "demo app"
    }
  10. Define subcomponents with @ContributesSubcomponent

    main

    The @ContributesSubcomponent annotation allows you to define a subcomponent in any Gradle module. The final subcomponent is generated when the parent component is merged.

    @ContributesSubcomponent(LoggedInScope::class)
    @SingleIn(LoggedInScope::class)
    interface RendererComponent {
    
        @ContributesSubcomponent.Factory(AppScope::class)
        interface Factory {
            fun createRendererComponent(): RendererComponent
        }
    }