/kotlin-inject

Dependency injection lib for kotlin

Primary LanguageKotlin

kotlin-inject

CircleCI

A compile-time dependency injection library for kotlin.

@Module abstract class AppModule {
    abstract val repo: Repository
    
    @Provides protected fun jsonParser() = JsonParser()

    @Provides protected val RealHttp.http: Http get() = this
}

interface Http
@Inject class RealHttp
@Inject class Api(private val http: Http, private val jsonParser: JsonParser)
@Inject class Repository(private val api: Api)
val appModule = AppModule::class.create()
val repo = appModule.repo

Download

plugins {
    id 'org.jetbrains.kotlin.jvm'
    id 'org.jetbrains.kotlin.kapt'
}

repositories {
  maven { url "https://oss.sonatype.org/content/repositories/snapshots/" }
}

dependencies {
    kapt "me.tatarka.inject:kotlin-inject-compiler:0.0.1-SNAPSHOT"
    implementation "me.tatarka.inject:kotlin-inject-runtime:0.0.1-SNAPSHOT"
}

Usage

Let's go through the above example line-by line and see what it's doing.

@Module abstract class Module {

The building block of kotlin-inject is a module which you declare with an @Module annotation on an abstract class. An implementation of this module will be generated for you.

    abstract val repo: Repository

In you module you can declare abstract read-only properties or functions to return an instance of a given type. This is where the magic happens. kotlin-inject will figure out how to construct that type for you in it's generated implementation. How does it know how to do this? There's a few ways:

    @Provides protected fun jsonParser() = JsonParser()

For external dependencies, you can declare a function or read-only property in the module to create an instance for a certain type. kotlin-inject will use the return type to provide this instance where it is requested.

    @Provides protected val RealHttp.http: Http get() = this

You can declare arguments to a providing function/property to help you construct your instance. Here we are taking in an instance of RealHttp and providing it for the interface Http. You can see a little sugar with this as the receiver type for an extension function/property counts as an argument. Another way to write this would be: fun provides(http: RealHttp): Http = http.

@Inject class RealHttp
@Inject class Api(private val http: Http, private val jsonParser: JsonParser)
@Inject class Repository(private val api: Api)

For your own dependencies you can simply annotate the class with @Inject. This will use the primary constructor to create an instance, no other configuration required!

val appModule = AppModule::class.create()
val repo = appModule.repo

Finally, you can create an instance of your module with the generated .create() extension function.

Features

Module Arguments

If you need to pass any instances into your module you can declare them as constructor args. You can then pass them into the generated create function. You can optionally annotate it with @Provides to provide the value to the dependency graph.

@Module abstract class MyModule(@Provides protected val foo: Foo)
MyModule::class.create(Foo())

If the argument is another module, you can annotate it with @Module and it's dependencies will also be available to the child module. This allows you to compose them into a graph.

@Module abstract class ParentModule {
    protected fun provideFoo(): Foo = ...
}

@Module abstract class ChildModule(@Module val parent: ParentModule) {
    abstract val foo: Foo
}
val parent = ParentModule::class.create()
val child = ChildModule::class.create(parent)

Type Alias Support

If you have multiple instances of the same type you want to differentiate, you can use type aliases. They will be treated as separate types for the purposes of injection.

typealias Dep1 = Dep
typealias Dep2 = Dep

@Modlue abstract class MyModlue {
    @Provides fun dep1(): Dep1 = Dep("one")
    @Provides fun dep2(): Dep2 = Dep("two")

    protected fun provides(dep1: Dep1, dep2: Dep1) = Thing(dep1, dep2)
}

@Inject class InjectedClass(dep1: Dep1, dep2: Dep2)

Scopes

By default kotlin-inject will create a new instance of a dependency each place it's injected. If you want to re-use an instance you can scope it to a module. The instance will live as long as that module does.

First create your scope annotation.

@Scope
annotation class MyScope

Then annotate your module with that scope annotation.

@MyScope @Module abstract class MyModule()

Finally, annotate your provides and @Inject classes with that scope.

@MyScope @Module abstract class MyModule {
    @MyScope @Provides
    protected fun provideFoo() = ...
}

@MyScope @Inject class Bar()

Multi-bindings

You can collect multiple bindings into a Map or Set by using the @IntoMap and @IntoSet annotations respectively.

For a set, return the type you want to put into a set, then you can inject or provide a Set<MyType>.

@Module abstract class MyModule {
    abstract val allFoos: Set<Foo>

    @IntoSet @Provides
    protected fun provideFoo1(): Foo = Foo("1")
    @IntoSet @Provdies
    protected fun provideFoo2(): Foo = Foo("2")
}

For a map, return a Pair<Key, Value>.

@Module abstract class MyModule {
    abstract val fooMap: Map<String, Foo>
    
    @IntoMap @Provides
    protected fun provideFoo1(): Pair<String, Foo> = "1" to Foo("1")
    @IntoMap @Provides
    protected fun provideFoo2(): Pair<String, Foo> = "2" to Foo("2")
}

Function Support

Sometimes you want to delay the creation of a dependency of provide additional params manually. You can do this by injecting a function that returns the dependency instead of the dependency directly.

The simplest case is you take no args, this gives you a function that can create the dep.

@Inject class Foo

@Inject class MyClass(fooCreator: () -> Foo) {
    init {
        val foo = fooCreator()
    }
}

If you define args, you can use these to assist the creation of the dependency. These are passed in as the last arguments to the dependency.

@Inject class Foo(bar: Bar, arg1: String, arg2: String)

@Inject class MyClass(fooCreator: (arg1: String, arg2: String) -> Foo) {
    init {
        val foo = fooCreator("1", "2")
    }
}

Lazy

Similarly, you can inject a Lazy<MyType> to construct and re-use and instance lazily.

@Inject class Foo

@Inject class MyClass(lazyFoo: Lazy<Foo>) {
    val foo by lazyFoo
}