
Kimchi
Kotlin Inject Merge Component Hints Intuitively
Kimchi is an Anvil-like KSP processor for kotlin-inject that lets you contribute bindings, modules, and subcomponents across multi-module projects to form your dependency injection graph without having to manually wire upstream.
Getting started
Getting started is easy! Just create a component abstract class or interface annotated with @MergeComponent like so
@MergeComponent(AppScope::class)
abstract class AppComponent
Kimchi generates a helpful extension function for easily creating the underlying merged component.
AppComponent::class.createAppComponent()
or if you define a companion object in your component
AppComponent.createAppComponent()
Now we have a component ready to collect contributions on the scope AppScope
Next, start contributing modules and bindings.
Binding
interface MenuRepository {
suspend fun getItems(): List<MenuItem>
}
@ContributesBinding(AppScope::class)
@Inject
class MenuRepositoryImpl() : MenuRepository {
override suspend fun : List<MenuItem> =
}
[!NOTE]
As of kotlin-inject 0.8.0 it will throw errors trying to instantiate implementations that are not explicitly exposed from a parent via a provision property.
With Kimchi this is fixed automatically by the setting:
com.r0adkll.kimchi.generateContributedBindingProvisions
which will automatically generate non-existing provision methods to expose all bindings to sub-graphs.
Multi-bindings
interface AppInitializer // { … }
@ContributesMultibinding(AppScope::class)
@Inject
class AnalyticsInitializer() : AppInitializer
@ContributesMultibinding(AppScope::class)
@Inject
class LoggingInitializer() : AppInitializer
@MergeComponent(AppScope::class)
{
initializers: Set<AppInitializer>
or you can specify a MapKey (StringKey, IntKey, LongKey, or custom) for mapped bindings
interface MenuSection
@StringKey("appetizers")
@ContributesMultibinding(AppScope::class)
:
:
{
menu: Map<String, MenuSection>
Modules / Component interfaces
@ContributesTo(AppScope::class)
interface CoroutinesModule {
@Provides
fun provideCoroutineDispatchers(): DispatcherProvider = DispatcherProvider(
io = Dispatchers.IO,
computation = Dispatchers.Default,
main = Dispatchers.Main,
)
}
This allows you to extend the upstream generated @Component with an interface full of bindings or other exposing provisions that you can control. This will look something like:
@Component
abstract class AppComponent : CoroutinesModule {
}
Subcomponent
@ContributesSubcomponent(
scope = UserScope::class,
parentScope = AppScope::class,
)
interface UserComponent {
@ContributesSubcomponent.Factory
interface Factory {
fun create(
userSession: UserSession,
) : UserComponent
}
}
This will merge all contributions to its scope, e.g. UserScope, and generate a subcomponent when its parent, e.g. AppScope, is processed. Outputting something like this:
@Component
abstract MergedAppComponent : UserComponent.Factory {
override fun create(userSession: UserSession): UserComponent =
MergedUserComponent::class.create(userSession, this)
@Component
abstract (
userSession: UserSession,
parent: MergedAppComponent,
) : UserComponent
}
Scopes
Kimchi uses scopes as markers to help the KSP processor connect the contributions you make to the target component you want to merge them to. The class AppScope from these examples can be represented like this:
object AppScope
These scope marker classes are independent of kotlin-inject's scopes but can be used together by creating a kotlin-inject scope wrapper like so:
@Scope
@Retention(AnnotationRetention.RUNTIME)
annotation class SingleIn(val scope: KClass<*>)
@SingleIn(AppScope::class)
:
Exclusions & Replacements
Components and bindings contributed with Kimchi can support replacements and overrides in a few ways. This is handy when you, for example, want to provide different bindings/contributions for instrumentation tests or different build flavors.
Using replaces = […] parameter
All @Contributes* annotations support specify an array of class references in their replaces parameter, like so:
@ContributesBinding(
scope = AppScope::class,
replaces = [MenuRepositoryImpl::class],
)
class TestMenuRepository : MenuRepository //…
Then if both TestMenuRepository and MenuRepositoryImpl are on the classpath, Kimchi will read the replaces argument and replace the bindings. This logic follows for all the other contribution annotations and merging.
Using rank parameter
When using @ContributesBinding, if you don't have access to the class/implementation that you want to replace/override on the classpath (e.g. if you are doing a sort of no-op setup), then you can define a rank integer that kimchi will use to determine priority when merging contributions of the same scope and bound on the classpath. The higher rank will always replace lower ranks. If components have the same rank then an error will be thrown.
e.g.
@ContributesBinding(
scope = AppScope::class,
rank = ContributesBinding.RANK_HIGHEST,
)
class NoOpMenuRepository : MenuRepository
[!NOTE]
It is always recommended to use replaces instead of rank whenever possible. The replaces parameter will always take precedence no matter what you set the rank value to.
Component Exclusions
You can explicitly declare contributed elements on the classpath from being merged into components using the excludes parameter on the @MergeComponent and @ContributesSubcomponent annotations, like so:
@MergeComponent(
scope = AppScope::class,
excludes = [ SomeLegacyModule::class ],
)
abstract class AppComponent
In a perfect world this feature shouldn't be needed. However, due to legacy setups, poor modularization, and other constraints of modern day software development, applications might need to use it.
Extending Kimchi
Every project is unique in how they setup their DI patterns and can often come with repetitive boilerplate for these patterns. Kimchi allows you to extend its capabilities by leveraging KSP's multi-round processing to generate your own boilerplate with Kimchi annotations.
Checkout the :circuit subproject for an example of this.
Setup

kotlin-inject

build.gradle.kts
plugins {
id("org.jetbrains.kotlin.jvm") version "2.2.0"
id("com.google.devtools.ksp") version "2.2.0-2.0.2"
}
dependencies {
implementation("me.tatarka.inject:kotlin-inject-runtime:0.8.0")
ksp("me.tatarka.inject:kotlin-inject-compiler-ksp:0.8.0")
implementation("com.r0adkll.kimchi:kimchi-annotations:<latest_version>")
ksp("com.r0adkll.kimchi:kimchi-compiler:<latest_version>")
implementation("com.r0adkll.kimchi:kimchi-circuit-annotations:<latest_version>")
ksp("com.r0adkll.kimchi:kimchi-circuit-compiler:<latest_version>")
}
Multiplatform
build.gradle.kts
Try this convenience function to mass apply ksp compilers to all targets
{
kmpExtension = extensions.getByType<KotlinMultiplatformExtension>()
dependencies {
kmpExtension.targets
.asSequence()
.filter { target ->
target.platformType != KotlinPlatformType.common
}
.forEach { target ->
add(
,
dependencyNotation,
)
}
}
}
: CharSequence = let<CharSequence, CharSequence> {
(it.isEmpty()) {
it
} it[].titlecase(
Locale.getDefault(),
) + it.substring()
}
License
Copyright 2024 r0adkll
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https:
Unless applicable law agreed to writing, software
distributed under the License distributed an BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express implied.
See the License the specific language governing permissions
limitations under the License.