Warning: this documentation is for library version 1.1.+ (witch is in alpha now), for older version check old version of documentation.
Simple DI with compile-time dependency graph validation for kotlin multiplatform.
It uses IR to generate code that finds or creates dependency in place of resolve()
call.
Limitations: all annotations required for generating code should be available in the same file as generated code. It can use methods and constructors from outside, but not annotations, because adding and removing annotations in other files would not trigger recompilation for generated code and combined with incremental compilation it would cause errors.
DI.kt is smaller and simpler than some solutions, but it verifies dependency graph in compile time like a serious DI framework.
DI.kt does not generate files during compilation, which makes compilation faster.
Because of its simplicity it might be useful for minimalistic DI in libraries and feature-modules, but it can be used in big project too.
Kotlin-inject - incredibly powerful DI framework with Dagger-like api;
Koin, Kodein-DI and PopKorn - service locators with great versatility, but without compile time error detection that we used to have in Dagger;
Dagger - most popular DI framework for Android, but it doesn't support multiplatform yet.
I forked kotlin multiplatform sample here and replaced di with DI.kt. It's clumsy, but it shows that library works on different platforms.
Here you can find more extensive project by @holdbetter that uses DI.kt.
Because library uses undocumented compiler api that often changes each library version works well only with specific kotlin versions, check table bellow to decide witch library version to use.
DI.kt version | Supported kotlin versions |
---|---|
1.1.0-kotlin-2.0.20 | 2.0.20 (k2 with new api) |
1.1.0-kotlin-2.0.0 | 2.0.0 - 2.0.10 (k2 with new api) |
1.1.0 | 1.8.20 - 1.9.20 (k2 with new api) |
1.1.0-kotlin-1.8.10 | 1.8.0 - 1.8.10 (k2 with new api) |
1.1.0-kotlin-1.7.21 | 1.7.20 - 1.7.21 (k2 with new api) |
1.0.4 | 1.8.20 - 1.8.21 (k2 with hacks) |
1.0.3 | 1.8.0 - 1.8.10 (k2 with hacks) |
1.0.2 | 1.7.0 - 1.7.21 |
1.0.1 | 1.6.2x |
1.0.0-alpha9 | 1.6.10 |
1.0.0-alpha7 | 1.6.0 |
Version 1.0.3 has limited support for k2, but requires hack to work properly - annotation @Suppress("NON_ABSTRACT_FUNCTION_WITH_NO_BODY") is required for module or each di function because plugin can't bypass function body check on k2 for now.
Version 1.1.0 has new api compatible with k2 without additional hacks. Old incompatible api is deprecated
In build.gradle file in module add plugin:
plugins {
id 'io.github.sergeshustoff.dikt' version '1.1.0'
}
Or use this if you only need base api without code generation (Injectable and InjectableSingleInScope interfaces):
dependencies {
implementation "io.github.sergeshustoff.dikt:dikt:1.1.0"
}
Create module and declare provided dependencies.
Use resolve()
function in places where you need your dependencies provided/created.
Use @ProvidesMembers
, @InjectByConstructors
and @InjectSingleByConstructors
annotations, or Injectable
and InjectableSingleInScope<Scope>
interfaces to control how dependencies are provided and what classes can be created by primary constructors.
@InjectSingleByConstructors(SomeSingleton::class)
@InjectByConstructors(SomethingElse::class)
class SomethingModule(
val externalDependency: Something,
) {
fun someSingleton(): SomeSingleton = resolve()
fun provideSomethingElse(): SomethingElse = resolve()
}
@InjectSingleByConstructors(SomeSingleton::class)
@InjectByConstructors(SomethingElse::class)
class SomethingModule(
val externalDependency: Something,
) {
private val someSingletonBackingField by lazy { SomeSingleton(externalDependency) }
fun someSingleton(): SomeSingleton = someSingletonBackingField
fun provideSomethingElse(): SomethingElse = SomethingElse(someSingletonBackingField)
}
Under the hood primary constructor will be called for SomethingElse and SomeSingleton. If constructor requires some parameters - they will be retrieved form module's properties and functions, and from function parameters.
resolve()
function can be called only in functions bodies. It shouldn't be called in constructors or property initializers
class Module {
fun something(): Something = resolve() // correct
fun processSomething() {
val something: Something = resolve() // correct
}
val something: Something = resolve() // incorrect
val something: Something by lazy { resolve() } // incorrect
val something: Something get() = resolve() // incorrect
init {
val something: Something = resolve() // incorrect
}
}
Library will fail compilation if there are any recursive dependencies in generated code, but there might be a situation when users code uses generated code to provide dependencies for injection. In this case it's better to write a function and list all needed dependencies in parameters instead of calling module functions directly:
class AutoInjectable(val manual: ManualInjectable)
class ManualInjectable(val injectable: AutoInjectable)
@InjectByConstructors(AutoInjectable::class)
class MyModule {
fun injectable(): AutoInjectable = resolve()
fun manualInjectable() = ManualInjectable(injectable()) // here is the recursion in users code that will not be detected by library
}
class AutoInjectable(val manual: ManualInjectable)
class ManualInjectable(val injectable: AutoInjectable)
@InjectByConstructors(AutoInjectable::class)
class MyModule {
fun injectable(): AutoInjectable = resolve()
fun manualInjectable(injectable: AutoInjectable) = ManualInjectable(injectable) // this way library detects recursion and fails compilation
}
Any class or object that has resolve()
calls somewhere inside is essentially a module. We don't need additional annotation for it, but if you need content of another 'module' provided as dependency in generated code, you need to mark a function or property returning that class as module using annotation @ProvidesMembers
.
If you need module to contain singletons of some scope mark it with @ModuleScopes(YourScope::class)
There are no true singletons in DI.kt, but instead you can use @InjectSingleByConstructors
. When resolving types listed in this annotation backing lazy field will be generated in module and instance of that type will be reused when it's resolved again. Such singleton instances persist as long as they resolved for the same instance of containing class (module).
Effectively it gives each module a scope of their own and makes the scoping more understandable.
Same effect as with @InjectSingleByConstructors
can be achieved using InjectableSingleInScope<Scope>
interface on dependency and @ModuleScopes(Scope::class)
annotation on module.
Dependencies directly implementing interface Injectable
will be provided by constructor when required. It is similar to @Inject annotation in java, but for compiler plugin compatibility with incremental compilation it had to be an interface (changing annotations will not cause dependant code to recompile, while changing supertypes will).
This interface can be used in project without dikt compiler plugin:
dependencies {
implementation 'io.github.sergeshustoff.dikt:dikt:1.1.0'
}
class SomeDependency : Injectable
class Something(val dependency: SomeDependency) : Injectable
class MyModule {
fun provideSomething(): Something = resolve()
}
class SomeDependency : Injectable
class Something(val dependency: SomeDependency) : Injectable
class MyModule {
fun provideSomething(): Something = Something(SomeDependency())
}
Similar to Injectable
interface, but for types implementing this interface directly can only be created in module annotated with @ModuleScopes(Scope::class) with the same type used for scope.
Only one instance of that type will be created in module, lazy backing field will be used to ensure that.
This interface can be used in project without dikt compiler plugin, same as Injectable
:
object Scope
class SomeSingleton : InjectableSingleInScope<Scope>
class Something(val dependency: SomeSingleton) : Injectable
@ModuleScopes(Scope::class)
class MyModule {
fun provideSomething(): Something = resolve()
}
object Scope
class SomeSingleton : InjectableSingleInScope<Scope>
class Something(val dependency: SomeSingleton) : Injectable
class MyModule {
private val _someSingleton by lazy { SomeSingleton() }
fun provideSomething(): Something = Something(_someSingleton)
}
For controlling how things created or provided by di there are a few annotations. Those annotations allow to use DI.kt without injectable code knowing anything about DI.kt.
Dependencies of types listed in this annotation parameters will be provided by constructor when required.
Might be applied to file, class or function.
class SomeDependency
class Something(val dependency: SomeDependency)
@InjectByConstructors(Something::class, SomeDependency::class)
class MyModule {
fun provideSomething(): Something = resolve()
}
class SomeDependency
class Something(val dependency: SomeDependency)
class MyModule {
fun provideSomething(): Something = Something(SomeDependency())
}
Dependencies of types listed in this annotation parameters will be provided by constructor when required, plus instance will be cached for future use in the same module by backing lazy field.
Might be applied to class.
class SomeDependency
class Something(val dependency: SomeDependency)
@InjectByConstructors(Something::class)
@InjectSingleByConstructors(SomeDependency::class)
class MyModule {
fun provideSomething(): Something = resolve()
}
class SomeDependency
class Something(val dependency: SomeDependency)
class MyModule {
private val cachedDependency by lazy { SomeDependency() }
fun provideSomething(): Something = Something(cachedDependency)
}
Indicates that all visible members of dependency returned from marked function or property can be used in dependency resolution.
If dependency is available in a nested module it will be used from there instead of calling a constructor even if it's marked with @InjectByConstructors
or implements Injectable
.
WARNING: This annotation doesn't work recursively. It means that any @ProvidesMembers
annotations in other classes or files will be ignored when generation dependency resolution.
class ExternalModule(val name: String)
class Something(val name: String)
@InjectByConstructors(Something::class)
class MyModule(
@ProvidesMembers private val external: ExternalModule
) {
fun provideSomething(): Something = resolve()
}
class ExternalModule(val name: String)
class Something(val name: String)
class MyModule(
private val external: ExternalModule
) {
fun provideSomething(): Something = Something(external.name)
}
Indicates that singletons for given scopes can be created in module marked with the annotation. Allows multiple scopes for a single module if needed.
Should be used in pair with InjectableSingleInScope interface on dependencies, scope should be the same in @ModuleScopes and in type parameter of InjectableSingleInScope
object Scope
class SomeSingleton : InjectableSingleInScope<Scope>
class Something(val dependency: SomeSingleton) : Injectable
@ModuleScopes(Scope::class)
class MyModule {
fun provideSomething(): Something = resolve()
}
object Scope
class SomeSingleton : InjectableSingleInScope<Scope>
class Something(val dependency: SomeSingleton) : Injectable
class MyModule {
private val _someSingleton by lazy { SomeSingleton() }
fun provideSomething(): Something = Something(_someSingleton)
}