Inside Koin 3.2 - New DSL
Hello Koin users 👋
We are delighted to announce the release of Koin 3.2. You can update to the latest version! Check the setup page.
The new Koin 3.2 release is ready 🎉 ! What's inside?
— InsertKoin.io (@insertkoin_io) May 11, 2022
- Kotlin 1.6.x
- Latest Android updates
- ViewModel Reworked ...
- Time API fixed
- New Constructor DSL
- Better Module Loading
- New Native Memory
- Ktor 2.0
- Compose 1.1 https://t.co/a1NoaXFnmE#AndroidDev #Kotlin
Today, I would like to zoom in on a great feature from this new release, that will significantly help us write smarter Koin configurations: New Constructor DSL 🔍
The Koin DSL ⚙️
Since the beginning, the core concept of Koin resides in providing a Kotlin DSL to help to write dependency injection configuration. Something easy to read and to write. The interest is to let you declare what you want, just through a simple function:
class MyClassA()
// Just a kotlin function to declare MyClassA
module {
single { MyClassA() }
}
With just a few keywords around (module, single, factory ...), you are able to declare mostly everything and focus yourself on delivering value, not on your DI tooling.
The Koin DSL is not using any kind of code analysis to understand what is provided behind your definitions. The key is to run the previously declared functions to build the collection of all your dependencies (also called "objects graph"). As we are using class constructors to build instances, we need to fill them with the right dependencies. This is why we need the get() operator, to ask Koin to retrieve dependencies for us:
class MyClassA()
class MyClassB(val a : MyClassA)
// Let Koin find dependencies for you with get()
module {
single { MyClassA() }
single { MyClassB(get()) }
}
Using functions to build instances allow us to work directly with pure Kotlin code and run dynamic behaviors (such as injecting parameters).
Downsides 💥
Since 2017, the time has shown that people like the Koin DSL. But it is not perfect, and the Koin community raised some points to be improved. Finally, Koin configuration files can break when you change the constructor of one component.
For example, let's add a new dependency "c" of type MyClassC to our class constructor MyClassB:
class MyClassA()
class MyClassC()
class MyClassB(val a : MyClassA, val c : MyClassC)
We need to add a declaration to our module for the MyClassC instance. But the module is not compiling anymore, because MyClassB's constructor has changed:
module {
single { MyClassA() }
single { MyClassC() }
// Breaks here, as we need to update the configuration
single { MyClassB(get()) }
}
We need to fix MyClassB's declaration to let our code compile, by adding a new get():
module {
single { MyClassA() }
single { MyClassC() }
// Fixed!
single { MyClassB(get(), get()) }
}
The other downside is the "proliferation" of the get() operator in all your configuration files for standard usage of Koin. Even more, for heavy configuration with lots of dependencies, this can result in having too much of it: " get() get() get() get() get() ... 😅"

A New Solution - The Constructor DSL 🎉
The new proposal introduced in the 3.2 release help declares components directly with their constructors. Our example above can now be rewritten like that:
module {
singleOf(::MyClassA)
singleOf(::MyClassC)
singleOf(::MyClassB)
}
As you can see, no more get()!
How does it work? You declare your component directly in parenthesis (no more function needed), using the :: operator. This way Koin knows to call your class constructor to build your component.
The magic behind is that Koin will fill your constructor arguments with get() function, without having to write it.
We have to introduce new DSL keywords for each type of component and keep the same DSL semantic: singleOf, factoryOf, scopedOf, viewModelOf ...
Easy to remind: We have kept the previous keyword and have added the "Of" at the end of the name. You can find them all here in the documentation reference: core extended DSL & android extended DSL. You can also find them on our Koin 3.2 Cheatsheet: https://blog.kotzilla.io/koin-3-2-annotations-cheat-sheets/.
The constructor DSL also allows to take into account any injected parameters:
class MyClassA(val id : String)
module {
// id will be passed as injected parameter
singleOf(::MyClassA)
}
// in calling code
val classA : MyClassA by inject { parametersOf("_id_")}
The other very good thing is this is forcing us to rethink our writing options (naming, additional bindings ...). We are already using a class constructor as arguments, we can't also ask options in the same way.
We decided to extract options in a sub lambda. This finally makes it overall very readable, by passing them like below:
interface MyComponent
class MyClassA() : MyComponent
module {
singleOf(::MyClassA) {
// options here
bind<MyComponent>()
named("_a_")
createdAtStart()
}
}
The options can be written with bind(), named() or even createdAtStart().
Even the bind() operator can be written directly without lambda:
interface MyComponent
class MyClassA() : MyComponent
module {
singleOf(::MyClassA) { bind<MyComponent>() }
// or even
singleOf(::MyClassA) bind<MyComponent>()
}
Finally, the only case where you will have to come back to classical DSL is when you will use qualifiers. Koin can't guess which dependency between definitions with the same type. Let's take an example with a Repository with 2 Datasources:
interface Datasource
class RemoteDatasource() : Datasource
class LocalDatasource() : Datasource
class Repository(val remote : Datasource, val local :Datasource)
module {
singleOf(::RemoteComponent) {
named("remote")
bind<Datasource>()
}
singleOf(::LocalComponent) {
named("local")
bind<Datasource>()
}
single { Repository(get(named("remote")),get(named("local")))
}
Be aware of one last thing: if your class has a constructor parameter with a default value, the DSL will try to resolve its dependency. Think perhaps having your default values in a configuration object.
This new DSL can cover many cases to help you write your Koin configuration. Don't hesitate to give us your feedback at @insertkoin_io.
Cheers 👋